Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add index_of, last_index_of, and for_each #71

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/for-each.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## for_each

`for_each(n)`

Executes a given function per each element in an `Enumerable` collection. Syntactic sugar for a for loop. This is an executing function.

**Parameters**

__func__ : the function, lambda or otherwise, to execute per each element

**Returns**

None

**Example**

<pre><code>
from py_linq import Enumerable

Enumerable([1 ,2 ,3]).for_each(print)
# 1
# 2
# 3

Enumerable([
{'value': 1},
{'value': 2},
{'value': 3}
]).for_each(lambda x: print(x['value']))
# 1
# 2
# 3

</code></pre>
33 changes: 33 additions & 0 deletions docs/index-of.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## index_of

`index_of(n)`

Returns the index of a given element in an `Enumerable` collection. If no matching element is found, None is returned. This is an executing function.

**Parameters**

__element__ : the element to search for and return the index of within the collection

**Returns**

The index of the given element, or None.

**Example**

<pre><code>
from py_linq import Enumerable

Enumerable([
{'value': 1},
{'value': 2},
{'value': 3}
]).index_of({'value': 2})
# 1

Enumerable([
{'value': 1},
{'value': 2},
{'value': 3}
]).index_of({'value': 4})
# None
</code></pre>
73 changes: 38 additions & 35 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,38 +42,41 @@ Once you have created an `Enumerable` instance, the LINQ methods will become ava
11. [intersect](/py-enumerable/intersect)
12. [except_](/py-enumerable/except)
13. [to_list](/py-enumerable/to-list)
14. [count](/py-enumerable/count)
15. [sum](/py-enumerable/sum)
16. [min](/py-enumerable/min)
17. [max](/py-enumerable/max)
18. [avg](/py-enumerable/avg)
19. [median](/py-enumerable/median)
20. [any](/py-enumerable/any)
21. [element_at](/py-enumerable/element-at)
22. [element_at_or_default](/py-enumerable/element-at-or-default)
23. [first](/py-enumerable/first)
24. [first_or_default](/py-enumerable/first-or-default)
25. [last](/py-enumerable/last)
26. [last_or_default](/py-enumerable/last-or-default)
27. [contains](/py-enumerable/contains)
28. [group_by](/py-enumerable/group-by)
29. [distinct](/py-enumerable/distinct)
30. [group_join](/py-enumerable/group-join)
31. [union](/py-enumerable/union)
32. [all](/py-enumerable/all)
33. [aggregate](/py-enumerable/aggregate)
34. [append](/py-enumerable/append)
35. [prepend](/py-enumerable/prepend)
36. [empty](/py-enumerable/empty)
37. [range](/py-enumerable/range)
38. [repeat](/py-enumerable/repeat)
39. [reverse](/py-enumerable/reverse)
40. [skip_last](/py-enumerable/skip_last)
41. [skip_while](/py-enumerable/skip_while)
42. [take_last](/py-enumerable/take_last)
43. [take_while](/py-enumerable/take_while)
44. [zip](/py-enumerable/zip)
45. [default_if_empty](/py-enumerable/default_if_empty)
46. [single](/py-enumerable/single)
47. [single_or_default](/py-enumerable/single-or-default)
48. [to_dictionary](/py-enumerable/to-dictionary)
14. [for_each](/py-enumerable/for-each)
15. [count](/py-enumerable/count)
16. [sum](/py-enumerable/sum)
17. [min](/py-enumerable/min)
18. [max](/py-enumerable/max)
19. [avg](/py-enumerable/avg)
20. [median](/py-enumerable/median)
21. [any](/py-enumerable/any)
22. [index_of](/py-enumerable/index-of)
23. [last_index_of](/py-enumerable/last-index-of)
24. [element_at](/py-enumerable/element-at)
25. [element_at_or_default](/py-enumerable/element-at-or-default)
26. [first](/py-enumerable/first)
27. [first_or_default](/py-enumerable/first-or-default)
28. [last](/py-enumerable/last)
29. [last_or_default](/py-enumerable/last-or-default)
30. [contains](/py-enumerable/contains)
31. [group_by](/py-enumerable/group-by)
32. [distinct](/py-enumerable/distinct)
33. [group_join](/py-enumerable/group-join)
34. [union](/py-enumerable/union)
35. [all](/py-enumerable/all)
36. [aggregate](/py-enumerable/aggregate)
37. [append](/py-enumerable/append)
38. [prepend](/py-enumerable/prepend)
39. [empty](/py-enumerable/empty)
40. [range](/py-enumerable/range)
40. [repeat](/py-enumerable/repeat)
41. [reverse](/py-enumerable/reverse)
42. [skip_last](/py-enumerable/skip_last)
43. [skip_while](/py-enumerable/skip_while)
44. [take_last](/py-enumerable/take_last)
45. [take_while](/py-enumerable/take_while)
46. [zip](/py-enumerable/zip)
47. [default_if_empty](/py-enumerable/default_if_empty)
48. [single](/py-enumerable/single)
49. [single_or_default](/py-enumerable/single-or-default)
50. [to_dictionary](/py-enumerable/to-dictionary)
36 changes: 36 additions & 0 deletions docs/last-index-of.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## last_index_of

`last_index_of(n)`

Returns the index of the last occurrence of a given element in an `Enumerable` collection. If no matching element is found, None is returned. This is an executing function.

**Parameters**

__element__ : the element to search for and return the last index of within the collection

**Returns**

The last index of the given element, or None.

**Example**

<pre><code>
from py_linq import Enumerable

Enumerable([
{'value': 1},
{'value': 2},
{'value': 3},
{'value': 1},
{'value': 2},
{'value': 3}
]).last_index_of({'value': 2})
# 4

Enumerable([
{'value': 1},
{'value': 2},
{'value': 3}
]).last_index_of({'value': 4})
# None
</code></pre>
36 changes: 36 additions & 0 deletions py_linq/py_linq.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ def select(self, func=lambda x: x):
"""
return SelectEnumerable(Enumerable(iter(self)), func)

def for_each(self, func=lambda x: x):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this code, it would be an executing function when it probably shouldn't be as there is no need. This would have to be tested, but I think this function could just be a wrapper around the output from the select function. For example:

def for_each(self, func=lambda x: x):
    return self.select(func)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was intentional, basically replicating this where input is Action and return is void: https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.foreach?view=net-6.0

"""
Execute a given function per each element
:param func: the function to execute
:return: None
"""
for x in self:
func(x)

def sum(self, func=lambda x: x):
"""
Returns the sum of af data elements
Expand Down Expand Up @@ -141,6 +150,33 @@ def median(self, func=lambda x: x):
else (float(result[i - 1]) + float(result[i])) / float(2)
)

def index_of(self, element):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method signature for this should include some lambda function to act as a selector for more complicated objects that do not have an __eq__ method defined (ie. how to define equality between 2 different instances of an object with same properties).

Also, what if your collection contains different types of objects (which is possible in an Enumerable collection). How do you define equality between these 2 different objects? This is what the lambda function would be for. As a default it could be set as lambda x: x

For example:

class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
points = Enumerable([Point(1, 2), Point(1, 3)])
assert points.index_of(Point(1,2)) is not None

This above example would fail in the assert because Point(1,2) are two different instances of Point even though they have the same x, y values.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I guess originally I was just leaving it up to them to implement eq or do a select first then index_of to account for what's needed beforehand, but an optional let's call it normalization function sounds like a good idea. I'll update in a bit.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit of an impasse on this, issue #72 (not specifically with regards to any() though) makes the behavior of this difficult to predict.

Here's what I wanted to do:

Updated index_of function:

    def index_of(self, element, func=lambda x: x, apply_func_to_element=False):
        """
        Returns the index of the first occurrence of a given element.

        :param element: the element for which to retrieve the index
        :param func: optional selector to apply to the collection as lambda expression for equality comparison
        :param apply_func_to_element: optional boolean flag indicating whether the selector lambda should be applied to the passed in element as well - default is False
        :return: Index of given element
        """
        for i, e in enumerate(self):
            if func(e) == (func(element) if apply_func_to_element else element):
                return i

        return None

Set up for testing:

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def __eq__(self, other):
        if isinstance(other, Rectangle):
            return self.length == other.length and self.width == other.width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * self.length + 2 * self.width


class Square(Rectangle):
    def __init__(self, length):
        super().__init__(length, length)

self.complex_types = Enumerable([
    Rectangle(1, 2),
    Rectangle(1, 4),
    Rectangle(2, 1),
    Rectangle(3, 4),
    Square(1),
    Square(2),
])

Test:

    def test_index_of_complex_types(self):
        self.assertEqual(4, self.complex_types.index_of(Square(1)))
        self.assertEqual(1, self.complex_types.index_of(4, lambda x: x.area()))
        self.assertEqual(1, self.complex_types.index_of(Square(2), lambda x: x.area(), True)     

Those assertions would pass if they were the only ones being executed, but since the Enumerable's order gets changed after a call to any of them, index_of becomes unreliable thereafter.

    def test_index_of_complex_types(self):
        # issue #72 will leave to undesirable behavior without an order_by happening first
        complex_types = self.complex_types.order_by(lambda x: (x.length, x.width))
        # -> [ Square(1), Rectangle(1,2), Rectangle(1,4), Rectangle(2,1), Square(2), Rectangle(3,4)]
        self.assertEqual(2, complex_types.index_of(4, lambda x: x.area()))

        # have to do it again unfortunately
        complex_types = self.complex_types.order_by(lambda x: (x.length, x.width))
        self.assertEqual(2, complex_types.index_of(Square(2), lambda x: x.area(), True))

What are your thoughts on this?

"""
Returns the index of the first occurrence of a given element.

:param element: the element for which to retrieve the index
:return: Index of given element
"""
for i, e in enumerate(self):
if e == element:
return i

return None

def last_index_of(self, element):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments as for index_of

"""
Returns the index of the last occurrence of a given element.

:param element: the element for which to retrieve the last index
:return: Index of last occurence of given element
"""
last_index = self.count() - 1
for i, e in enumerate(self.reverse()):
if e == element:
return last_index - i

return None

def element_at(self, n):
"""
Returns element at given index.
Expand Down
38 changes: 33 additions & 5 deletions tests/test_functions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from io import StringIO
from py_linq.py_linq import SelectEnumerable, WhereEnumerable
from unittest import TestCase
from unittest.mock import patch
from py_linq import Enumerable
from tests import _empty, _simple, _complex, _locations
from py_linq.exceptions import (
Expand Down Expand Up @@ -94,6 +96,20 @@ def test_select(self):
.to_list(),
)

def test_for_each(self):

with patch("sys.stdout", new=StringIO()) as fake_out:
self.simple.for_each(print)
self.assertEqual(fake_out.getvalue(), "1\n2\n3\n")

with patch("sys.stdout", new=StringIO()) as fake_out:
self.complex.for_each(lambda x: print(x["value"]))
self.assertEqual(fake_out.getvalue(), "1\n2\n3\n")

output = []
self.complex.for_each(lambda x: output.append(x))
self.assertListEqual(output, self.complex.to_list())

def test_min(self):
self.assertRaises(NoElementsError, self.empty.min)
self.assertEqual(1, self.simple.min())
Expand Down Expand Up @@ -200,6 +216,16 @@ def test_median(self):
self.assertEqual(median, self.simple.median())
self.assertEqual(median, self.complex.median(lambda x: x["value"]))

def test_index_of(self):
self.assertEqual(1, self.simple.index_of(2))
self.assertEqual(1, self.complex.index_of({"value": 2}))

def test_last_index_of(self):
self.assertEqual(4, self.simple.concat(Enumerable(_simple)).last_index_of(2))
self.assertEqual(
4, self.complex.concat(Enumerable(_complex)).last_index_of({"value": 2})
)

def test_skip(self):
self.assertListEqual([], Enumerable().skip(2).to_list())
self.assertListEqual([], Enumerable([1, 2, 3]).skip(3).to_list())
Expand Down Expand Up @@ -849,11 +875,13 @@ def test_to_dictionary(self):
test = Enumerable(["ab", "bc", "cd", "de"]).to_dictionary(lambda t: t[0])
self.assertDictEqual(test, {"a": "ab", "b": "bc", "c": "cd", "d": "de"})

test = Enumerable([
[0, 1, 2],
[3, 4, 5],
[6, 7, 8]
]).to_dictionary(lambda t: t[0], lambda t: t[1:])
test = Enumerable(
[
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
]
).to_dictionary(lambda t: t[0], lambda t: t[1:])
self.assertDictEqual(test, {0: [1, 2], 3: [4, 5], 6: [7, 8]})

def test_zip(self):
Expand Down