From dbdc1a5036e41b16727e88f63471001fc6d18eac Mon Sep 17 00:00:00 2001 From: mic1on Date: Sat, 14 Mar 2026 12:14:19 +0800 Subject: [PATCH] feat(list): add 10 new utility functions - group_by: group elements by key function - find: find first element matching predicate - find_index: find index of first matching element - intersection: get intersection of multiple arrays - partition: split array into two groups - take/take_right: get first/last n elements - drop/drop_right: skip first/last n elements - nth: get element at index (supports negative) With comprehensive unit tests (41 test cases) --- src/usepy/list/__init__.py | 21 +++- src/usepy/list/drop.py | 49 ++++++++ src/usepy/list/find.py | 29 +++++ src/usepy/list/find_index.py | 28 +++++ src/usepy/list/group_by.py | 30 +++++ src/usepy/list/intersection.py | 39 ++++++ src/usepy/list/nth.py | 34 +++++ src/usepy/list/partition.py | 34 +++++ src/usepy/list/take.py | 49 ++++++++ tests/test_list_new.py | 223 +++++++++++++++++++++++++++++++++ 10 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 src/usepy/list/drop.py create mode 100644 src/usepy/list/find.py create mode 100644 src/usepy/list/find_index.py create mode 100644 src/usepy/list/group_by.py create mode 100644 src/usepy/list/intersection.py create mode 100644 src/usepy/list/nth.py create mode 100644 src/usepy/list/partition.py create mode 100644 src/usepy/list/take.py create mode 100644 tests/test_list_new.py diff --git a/src/usepy/list/__init__.py b/src/usepy/list/__init__.py index 8854ef3..74a7d06 100644 --- a/src/usepy/list/__init__.py +++ b/src/usepy/list/__init__.py @@ -16,6 +16,14 @@ from .zip_dict import zip_dict from .first import first from .last import last +from .group_by import group_by +from .find import find +from .find_index import find_index +from .intersection import intersection +from .partition import partition +from .take import take, take_right +from .drop import drop, drop_right +from .nth import nth __all__ = [ @@ -37,4 +45,15 @@ "zip_dict", "first", "last", -] + # New functions + "group_by", + "find", + "find_index", + "intersection", + "partition", + "take", + "take_right", + "drop", + "drop_right", + "nth", +] \ No newline at end of file diff --git a/src/usepy/list/drop.py b/src/usepy/list/drop.py new file mode 100644 index 0000000..e1c7d81 --- /dev/null +++ b/src/usepy/list/drop.py @@ -0,0 +1,49 @@ +from typing import TypeVar, Sequence, List + +T = TypeVar('T') + + +def drop(arr: Sequence[T], n: int = 1) -> List[T]: + """ + Drops the first n elements from a sequence. + + Args: + arr (Sequence[T]): The sequence to drop from. + n (int): The number of elements to drop. Defaults to 1. + + Returns: + List[T]: A new list without the first n elements. + + Examples: + >>> drop([1, 2, 3, 4, 5], 2) + [3, 4, 5] + >>> drop([1, 2, 3], 0) + [1, 2, 3] + >>> drop([1, 2, 3], 10) + [] + """ + return list(arr[n:]) + + +def drop_right(arr: Sequence[T], n: int = 1) -> List[T]: + """ + Drops the last n elements from a sequence. + + Args: + arr (Sequence[T]): The sequence to drop from. + n (int): The number of elements to drop from the end. Defaults to 1. + + Returns: + List[T]: A new list without the last n elements. + + Examples: + >>> drop_right([1, 2, 3, 4, 5], 2) + [1, 2, 3] + >>> drop_right([1, 2, 3], 0) + [1, 2, 3] + >>> drop_right([1, 2, 3], 10) + [] + """ + if n <= 0: + return list(arr) + return list(arr[:-n]) if n < len(arr) else [] \ No newline at end of file diff --git a/src/usepy/list/find.py b/src/usepy/list/find.py new file mode 100644 index 0000000..2853f17 --- /dev/null +++ b/src/usepy/list/find.py @@ -0,0 +1,29 @@ +from typing import TypeVar, Sequence, Callable, Optional + +T = TypeVar('T') + + +def find(arr: Sequence[T], predicate: Callable[[T], bool], default: Optional[T] = None) -> Optional[T]: + """ + Finds the first element in a sequence that satisfies a predicate function. + + Args: + arr (Sequence[T]): The sequence to search. + predicate (Callable[[T], bool]): A function that returns True for the desired element. + default (Optional[T]): The value to return if no element is found. Defaults to None. + + Returns: + Optional[T]: The first element that satisfies the predicate, or default if not found. + + Examples: + >>> find([1, 2, 3, 4], lambda x: x > 2) + 3 + >>> find([1, 2, 3], lambda x: x > 10, default=0) + 0 + >>> find([], lambda x: x > 0) + None + """ + for item in arr: + if predicate(item): + return item + return default \ No newline at end of file diff --git a/src/usepy/list/find_index.py b/src/usepy/list/find_index.py new file mode 100644 index 0000000..843f88e --- /dev/null +++ b/src/usepy/list/find_index.py @@ -0,0 +1,28 @@ +from typing import TypeVar, Sequence, Callable + +T = TypeVar('T') + + +def find_index(arr: Sequence[T], predicate: Callable[[T], bool]) -> int: + """ + Finds the index of the first element in a sequence that satisfies a predicate function. + + Args: + arr (Sequence[T]): The sequence to search. + predicate (Callable[[T], bool]): A function that returns True for the desired element. + + Returns: + int: The index of the first element that satisfies the predicate, or -1 if not found. + + Examples: + >>> find_index([1, 2, 3, 4], lambda x: x > 2) + 2 + >>> find_index([1, 2, 3], lambda x: x > 10) + -1 + >>> find_index([], lambda x: x > 0) + -1 + """ + for i, item in enumerate(arr): + if predicate(item): + return i + return -1 \ No newline at end of file diff --git a/src/usepy/list/group_by.py b/src/usepy/list/group_by.py new file mode 100644 index 0000000..f69ca1f --- /dev/null +++ b/src/usepy/list/group_by.py @@ -0,0 +1,30 @@ +from typing import TypeVar, Sequence, Callable, Dict, List + +T = TypeVar('T') +K = TypeVar('K') + + +def group_by(arr: Sequence[T], key_func: Callable[[T], K]) -> Dict[K, List[T]]: + """ + Groups elements of a sequence by a key generated from a function. + + Args: + arr (Sequence[T]): The sequence to group. + key_func (Callable[[T], K]): A function that generates a key from each element. + + Returns: + Dict[K, List[T]]: A dictionary where keys are the generated keys and values are lists of elements. + + Examples: + >>> group_by([1, 2, 3, 4, 5], lambda x: x % 2) + {1: [1, 3, 5], 0: [2, 4]} + >>> group_by(['apple', 'banana', 'apricot'], lambda x: x[0]) + {'a': ['apple', 'apricot'], 'b': ['banana']} + """ + result: Dict[K, List[T]] = {} + for item in arr: + key = key_func(item) + if key not in result: + result[key] = [] + result[key].append(item) + return result \ No newline at end of file diff --git a/src/usepy/list/intersection.py b/src/usepy/list/intersection.py new file mode 100644 index 0000000..c546ad5 --- /dev/null +++ b/src/usepy/list/intersection.py @@ -0,0 +1,39 @@ +from typing import TypeVar, Sequence, List, Iterable + +T = TypeVar('T') + + +def intersection(*arrays: Iterable[T]) -> List[T]: + """ + Returns the intersection of multiple arrays. + + The order of the result is determined by the first array. + + Args: + *arrays (Iterable[T]): The arrays to intersect. + + Returns: + List[T]: A new array of values that are in all given arrays. + + Examples: + >>> intersection([1, 2, 3], [2, 3, 4], [2, 3, 5]) + [2, 3] + >>> intersection([1, 2, 3], [4, 5, 6]) + [] + >>> intersection([1, 2, 2, 3], [2, 3]) + [2, 3] + """ + if not arrays: + return [] + + # Convert all arrays to sets for efficient lookup + sets = [set(arr) for arr in arrays[1:]] + + result = [] + seen = set() + for item in arrays[0]: + if item not in seen and all(item in s for s in sets): + result.append(item) + seen.add(item) + + return result \ No newline at end of file diff --git a/src/usepy/list/nth.py b/src/usepy/list/nth.py new file mode 100644 index 0000000..b7f77bf --- /dev/null +++ b/src/usepy/list/nth.py @@ -0,0 +1,34 @@ +from typing import TypeVar, Sequence, Optional + +T = TypeVar('T') + + +def nth(arr: Sequence[T], n: int, default: Optional[T] = None) -> Optional[T]: + """ + Gets the element at index n of a sequence. Supports negative indices. + + Args: + arr (Sequence[T]): The sequence to query. + n (int): The index of the element to return. Negative indices count from the end. + default (Optional[T]): The value to return if index is out of bounds. Defaults to None. + + Returns: + Optional[T]: The element at index n, or default if out of bounds. + + Examples: + >>> nth([1, 2, 3, 4, 5], 1) + 2 + >>> nth([1, 2, 3, 4, 5], -1) + 5 + >>> nth([1, 2, 3], 10, default=0) + 0 + >>> nth([], 0) + None + """ + if not arr: + return default + + try: + return arr[n] + except IndexError: + return default \ No newline at end of file diff --git a/src/usepy/list/partition.py b/src/usepy/list/partition.py new file mode 100644 index 0000000..fb45f27 --- /dev/null +++ b/src/usepy/list/partition.py @@ -0,0 +1,34 @@ +from typing import TypeVar, Sequence, Callable, Tuple, List + +T = TypeVar('T') + + +def partition(arr: Sequence[T], predicate: Callable[[T], bool]) -> Tuple[List[T], List[T]]: + """ + Partitions a sequence into two lists based on a predicate function. + + Args: + arr (Sequence[T]): The sequence to partition. + predicate (Callable[[T], bool]): A function that returns True for elements to be in the first list. + + Returns: + Tuple[List[T], List[T]]: A tuple of two lists - elements that satisfy the predicate and those that don't. + + Examples: + >>> partition([1, 2, 3, 4, 5], lambda x: x % 2 == 0) + ([2, 4], [1, 3, 5]) + >>> partition(['a', 'bb', 'ccc'], lambda x: len(x) > 1) + (['bb', 'ccc'], ['a']) + >>> partition([], lambda x: x > 0) + ([], []) + """ + matches: List[T] = [] + rest: List[T] = [] + + for item in arr: + if predicate(item): + matches.append(item) + else: + rest.append(item) + + return matches, rest \ No newline at end of file diff --git a/src/usepy/list/take.py b/src/usepy/list/take.py new file mode 100644 index 0000000..03014fc --- /dev/null +++ b/src/usepy/list/take.py @@ -0,0 +1,49 @@ +from typing import TypeVar, Sequence, List + +T = TypeVar('T') + + +def take(arr: Sequence[T], n: int = 1) -> List[T]: + """ + Takes the first n elements from a sequence. + + Args: + arr (Sequence[T]): The sequence to take from. + n (int): The number of elements to take. Defaults to 1. + + Returns: + List[T]: A new list with the first n elements. + + Examples: + >>> take([1, 2, 3, 4, 5], 2) + [1, 2] + >>> take([1, 2, 3], 0) + [] + >>> take([1, 2, 3], 10) + [1, 2, 3] + """ + return list(arr[:n]) + + +def take_right(arr: Sequence[T], n: int = 1) -> List[T]: + """ + Takes the last n elements from a sequence. + + Args: + arr (Sequence[T]): The sequence to take from. + n (int): The number of elements to take from the end. Defaults to 1. + + Returns: + List[T]: A new list with the last n elements. + + Examples: + >>> take_right([1, 2, 3, 4, 5], 2) + [4, 5] + >>> take_right([1, 2, 3], 0) + [] + >>> take_right([1, 2, 3], 10) + [1, 2, 3] + """ + if n <= 0: + return [] + return list(arr[-n:]) if n < len(arr) else list(arr) \ No newline at end of file diff --git a/tests/test_list_new.py b/tests/test_list_new.py new file mode 100644 index 0000000..a951228 --- /dev/null +++ b/tests/test_list_new.py @@ -0,0 +1,223 @@ +import pytest +from usepy.list import ( + group_by, + find, + find_index, + intersection, + partition, + take, + take_right, + drop, + drop_right, + nth, +) + + +class TestGroupBy: + """Tests for group_by() function""" + + def test_group_by_even_odd(self): + """Test grouping by even/odd""" + result = group_by([1, 2, 3, 4, 5], lambda x: x % 2) + assert result == {1: [1, 3, 5], 0: [2, 4]} + + def test_group_by_first_letter(self): + """Test grouping by first letter""" + result = group_by(['apple', 'banana', 'apricot', 'blueberry'], lambda x: x[0]) + assert result == {'a': ['apple', 'apricot'], 'b': ['banana', 'blueberry']} + + def test_group_by_empty(self): + """Test grouping empty list""" + assert group_by([], lambda x: x) == {} + + def test_group_by_length(self): + """Test grouping by string length""" + result = group_by(['a', 'bb', 'c', 'dd'], len) + assert result == {1: ['a', 'c'], 2: ['bb', 'dd']} + + +class TestFind: + """Tests for find() function""" + + def test_find_element(self): + """Test finding an element""" + assert find([1, 2, 3, 4], lambda x: x > 2) == 3 + + def test_find_not_found(self): + """Test element not found returns None""" + assert find([1, 2, 3], lambda x: x > 10) is None + + def test_find_with_default(self): + """Test element not found returns default""" + assert find([1, 2, 3], lambda x: x > 10, default=0) == 0 + + def test_find_empty(self): + """Test finding in empty list""" + assert find([], lambda x: x > 0) is None + + def test_find_first_match(self): + """Test returns first matching element""" + assert find([1, 2, 3, 4, 5], lambda x: x > 2) == 3 + + +class TestFindIndex: + """Tests for find_index() function""" + + def test_find_index(self): + """Test finding index""" + assert find_index([1, 2, 3, 4], lambda x: x > 2) == 2 + + def test_find_index_not_found(self): + """Test index not found returns -1""" + assert find_index([1, 2, 3], lambda x: x > 10) == -1 + + def test_find_index_empty(self): + """Test finding index in empty list""" + assert find_index([], lambda x: x > 0) == -1 + + def test_find_index_first_match(self): + """Test returns first matching index""" + assert find_index([1, 2, 3, 2, 1], lambda x: x == 2) == 1 + + +class TestIntersection: + """Tests for intersection() function""" + + def test_intersection_two_arrays(self): + """Test intersection of two arrays""" + assert intersection([1, 2, 3], [2, 3, 4]) == [2, 3] + + def test_intersection_three_arrays(self): + """Test intersection of three arrays""" + assert intersection([1, 2, 3], [2, 3, 4], [2, 3, 5]) == [2, 3] + + def test_intersection_no_common(self): + """Test no common elements""" + assert intersection([1, 2, 3], [4, 5, 6]) == [] + + def test_intersection_empty(self): + """Test intersection with empty""" + assert intersection([], [1, 2]) == [] + assert intersection([1, 2], []) == [] + + def test_intersection_duplicates(self): + """Test intersection preserves first array order without duplicates""" + assert intersection([1, 2, 2, 3], [2, 3]) == [2, 3] + + +class TestPartition: + """Tests for partition() function""" + + def test_partition_even_odd(self): + """Test partitioning by even/odd""" + matches, rest = partition([1, 2, 3, 4, 5], lambda x: x % 2 == 0) + assert matches == [2, 4] + assert rest == [1, 3, 5] + + def test_partition_all_match(self): + """Test all elements match""" + matches, rest = partition([2, 4, 6], lambda x: x % 2 == 0) + assert matches == [2, 4, 6] + assert rest == [] + + def test_partition_none_match(self): + """Test no elements match""" + matches, rest = partition([1, 3, 5], lambda x: x % 2 == 0) + assert matches == [] + assert rest == [1, 3, 5] + + def test_partition_empty(self): + """Test partitioning empty list""" + matches, rest = partition([], lambda x: x > 0) + assert matches == [] + assert rest == [] + + +class TestTake: + """Tests for take() and take_right() functions""" + + def test_take(self): + """Test taking first n elements""" + assert take([1, 2, 3, 4, 5], 2) == [1, 2] + + def test_take_zero(self): + """Test taking zero elements""" + assert take([1, 2, 3], 0) == [] + + def test_take_more_than_length(self): + """Test taking more than length""" + assert take([1, 2, 3], 10) == [1, 2, 3] + + def test_take_default(self): + """Test taking with default n=1""" + assert take([1, 2, 3]) == [1] + + def test_take_right(self): + """Test taking last n elements""" + assert take_right([1, 2, 3, 4, 5], 2) == [4, 5] + + def test_take_right_zero(self): + """Test taking zero elements from right""" + assert take_right([1, 2, 3], 0) == [] + + def test_take_right_more_than_length(self): + """Test taking more than length from right""" + assert take_right([1, 2, 3], 10) == [1, 2, 3] + + +class TestDrop: + """Tests for drop() and drop_right() functions""" + + def test_drop(self): + """Test dropping first n elements""" + assert drop([1, 2, 3, 4, 5], 2) == [3, 4, 5] + + def test_drop_zero(self): + """Test dropping zero elements""" + assert drop([1, 2, 3], 0) == [1, 2, 3] + + def test_drop_more_than_length(self): + """Test dropping more than length""" + assert drop([1, 2, 3], 10) == [] + + def test_drop_default(self): + """Test dropping with default n=1""" + assert drop([1, 2, 3]) == [2, 3] + + def test_drop_right(self): + """Test dropping last n elements""" + assert drop_right([1, 2, 3, 4, 5], 2) == [1, 2, 3] + + def test_drop_right_zero(self): + """Test dropping zero elements from right""" + assert drop_right([1, 2, 3], 0) == [1, 2, 3] + + def test_drop_right_more_than_length(self): + """Test dropping more than length from right""" + assert drop_right([1, 2, 3], 10) == [] + + +class TestNth: + """Tests for nth() function""" + + def test_nth_positive(self): + """Test getting nth element (positive index)""" + assert nth([1, 2, 3, 4, 5], 1) == 2 + + def test_nth_negative(self): + """Test getting nth element (negative index)""" + assert nth([1, 2, 3, 4, 5], -1) == 5 + assert nth([1, 2, 3, 4, 5], -2) == 4 + + def test_nth_out_of_bounds(self): + """Test out of bounds returns default""" + assert nth([1, 2, 3], 10) is None + assert nth([1, 2, 3], 10, default=0) == 0 + + def test_nth_empty(self): + """Test getting nth from empty list""" + assert nth([], 0) is None + + def test_nth_first(self): + """Test getting first element""" + assert nth([1, 2, 3], 0) == 1 \ No newline at end of file