Skip to content
Merged
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Changelog

## Version 0.4.0 - 0.4.1
## Version 0.4.0 - 0.4.2

- Implemented R's `split()` utility to split a sequence by a grouping factor.
- Turn `match()` into a generic for specialization by other BiocPy classes.
- Implement `duplicated()` generic for simple sequences and Factors.
- Implement the `order()` verb for basic sequences, Factors.
- Added verbose parameter to `is_package_installed`.
- Added `setdiff` function for simple sequences and factors.

## Version 0.3.0 - 0.3.4

Expand Down
8 changes: 8 additions & 0 deletions src/biocutils/Factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .factorize import factorize
from .is_list_of_type import is_list_of_type
from .is_missing_scalar import is_missing_scalar
from .map_to_index import DUPLICATE_METHOD
from .match import match
from .Names import Names, _combine_names, _name_to_position, _sanitize_names
from .normalize_subscript import (
Expand All @@ -19,6 +20,7 @@
normalize_subscript,
)
from .print_truncated import print_truncated_list
from .setdiff import _setdiff_internal, setdiff
from .StringList import StringList
from .subset_sequence import subset_sequence

Expand Down Expand Up @@ -876,3 +878,9 @@ def _combine_factors(*x: Factor):
names=_combine_names(*x, get_names=lambda x: x.get_names()),
_validate=False,
)


@setdiff.register(Factor)
def _setdiff_Factor(x: Factor, *other: Sequence, duplicate_method: DUPLICATE_METHOD = "first") -> Factor:
res = _setdiff_internal(x.as_list(), *other, duplicate_method=duplicate_method)
return type(x).from_sequence(res, levels=x.get_levels(), ordered=x.get_ordered())
1 change: 1 addition & 0 deletions src/biocutils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from .normalize_subscript import normalize_subscript, SubscriptTypes
from .print_truncated import print_truncated, print_truncated_dict, print_truncated_list
from .print_wrapped_table import create_floating_names, print_type, print_wrapped_table, truncate_strings
from .setdiff import setdiff
from .union import union

from .combine import combine
Expand Down
65 changes: 65 additions & 0 deletions src/biocutils/setdiff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from collections.abc import Sequence

from .is_missing_scalar import is_missing_scalar
from .map_to_index import DUPLICATE_METHOD


from functools import singledispatch


@singledispatch
def _setdiff_internal(first: Sequence, *other: Sequence, duplicate_method: DUPLICATE_METHOD = "first") -> list:
present = set()
for i in range(len(other)):
for f in other[i]:
if not is_missing_scalar(f):
present.add(f)

output = []

def handler(f):
if not is_missing_scalar(f) and f not in present:
output.append(f)
present.add(f)

if duplicate_method == "first":
for f in first:
handler(f)
else:
for f in reversed(first):
handler(f)
output.reverse()

return output


def setdiff(*x: Sequence, duplicate_method: DUPLICATE_METHOD = "first") -> list:
"""Identify the set difference of values in multiple sequences, preserves
the order of values in the first sequence.

This is a :py:func:`~functools.singledispatch` generic, allowing developers
to specify custom methods for their own classes.

Args:
x:
One or more sequences of interest containing hashable values.
Ignores missing values as defined in
:py:meth:`~biocutils.is_missing_scalar.is_missing_scalar`.

duplicate_method:
Whether to keep the first or last occurrence of duplicated values
when preserving order in the first sequence.

Returns:
Difference of values in the first sequence but not in the others.
If no sequences are provided, an empty list is returned.
If one sequence is provided, the unique values in the sequence are returned.
"""
nargs = len(x)
if nargs == 0:
return []

return _setdiff_internal(x[0], *x[1:], duplicate_method=duplicate_method)


setdiff.register = _setdiff_internal.register
72 changes: 72 additions & 0 deletions tests/test_setdiff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from biocutils import setdiff


def test_setdiff_simple():
assert setdiff() == []

y = ["B", "C", "A", "D", "E"]
out = setdiff(y)
assert out == y

out = setdiff(y, ["A", "C", "E"])
assert out == ["B", "D"]

out = setdiff(y, ["A", "C"], ["E"])
assert out == ["B", "D"]


def test_setdiff_duplicates():
# Deduplicates elements in the first sequence
out = setdiff(["B", "B", "C", "A", "D", "D", "E"], ["A", "A", "F", "F"])
assert out == ["B", "C", "D", "E"]

out = setdiff(["B", "B", "C", "A", "D", "D", "E"], ["A", "A", "F", "F"], duplicate_method="last")
assert out == ["B", "C", "D", "E"]

# Switches the order of B being reported.
out = setdiff(
["C", "A", "D", "B", "E", "B"], ["A", "C", "E", "F"], duplicate_method="last"
)
assert out == ["D", "B"]

out = setdiff(
["C", "A", "D", "B", "E", "B"], ["A", "C", "E", "F"]
)
assert out == ["D", "B"]


def test_setdiff_none():
y = ["B", None, "C", "A", None, "D", "E"]
out = setdiff(y)
assert out == ["B", "C", "A", "D", "E"]

out = setdiff(y, ["A", None, "C"])
assert out == ["B", "D", "E"]


def test_setdiff_factor():
from biocutils import Factor
f1 = Factor.from_sequence(["B", "B", "C", "A", "D", "D", "E"])
f2 = Factor.from_sequence(["A", "A", "F", "F"])

out = setdiff(f1, f2)
assert isinstance(out, Factor)
assert out.as_list() == ["B", "C", "D", "E"]
assert out.get_levels() == f1.get_levels()

out = setdiff(f1, f2, duplicate_method="last")
assert isinstance(out, Factor)
assert out.as_list() == ["B", "C", "D", "E"]
assert out.get_levels() == f1.get_levels()

f3 = Factor.from_sequence(["C", "A", "D", "B", "E", "B"])
f4 = Factor.from_sequence(["A", "C", "E", "F"])
out = setdiff(f3, f4, duplicate_method="last")
assert isinstance(out, Factor)
assert out.as_list() == ["D", "B"]
assert out.get_levels() == f3.get_levels()

out = setdiff(f3, f4)
assert isinstance(out, Factor)
assert out.as_list() == ["D", "B"]
assert out.get_levels() == f3.get_levels()
Loading