diff --git a/docs/reference/accessors.rst b/docs/reference/accessors.rst index 9682181..5927321 100644 --- a/docs/reference/accessors.rst +++ b/docs/reference/accessors.rst @@ -12,4 +12,7 @@ Accessors ArrayAccessor.union ArrayAccessor.intersection ArrayAccessor.difference - ArrayAccessor.symmetric_difference \ No newline at end of file + ArrayAccessor.symmetric_difference + ArrayAccessor.isdisjoint + ArrayAccessor.issuperset + ArrayAccessor.issubset \ No newline at end of file diff --git a/docs/reference/interval.rst b/docs/reference/interval.rst index bd67928..a8ec10c 100644 --- a/docs/reference/interval.rst +++ b/docs/reference/interval.rst @@ -12,4 +12,6 @@ Interval union intersection difference - symmetric_difference \ No newline at end of file + symmetric_difference + issuperset + issubset \ No newline at end of file diff --git a/docs/reference/package.rst b/docs/reference/package.rst index bfc4708..b0f4af1 100644 --- a/docs/reference/package.rst +++ b/docs/reference/package.rst @@ -14,4 +14,7 @@ Top level functions union intersection difference - symmetric_difference \ No newline at end of file + symmetric_difference + isdisjoint + issuperset + issubset \ No newline at end of file diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index 7873be7..ff5bc83 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -4,6 +4,24 @@ Release notes ======================== + +ADD UNRELEASED CHANGES ABOVE THIS LINE + + +**v0.2.0 2021-10-15** + +Added the following methods + +- :meth:`piso.isdisjoint` +- :meth:`piso.issuperset` +- :meth:`piso.issubset` +- :meth:`ArrayAccessor.isdisjoint() ` +- :meth:`ArrayAccessor.issuperset() ` +- :meth:`ArrayAccessor.issubset() ` +- :meth:`piso.interval.issuperset` +- :meth:`piso.interval.issubset` + + **v0.1.0 2021-10-10** The following methods are included in the initial release of `piso` @@ -13,7 +31,12 @@ The following methods are included in the initial release of `piso` - :meth:`piso.intersection` - :meth:`piso.difference` - :meth:`piso.symmetric_difference` +- :meth:`ArrayAccessor.union() ` +- :meth:`ArrayAccessor.intersection() ` +- :meth:`ArrayAccessor.difference() ` +- :meth:`ArrayAccessor.symmetric_difference() ` - :meth:`piso.interval.union` - :meth:`piso.interval.intersection` - :meth:`piso.interval.difference` - :meth:`piso.interval.symmetric_difference` + diff --git a/piso/__init__.py b/piso/__init__.py index f9f67f2..c589f89 100644 --- a/piso/__init__.py +++ b/piso/__init__.py @@ -1,4 +1,12 @@ -from piso.intervalarray import difference, intersection, symmetric_difference, union +from piso.intervalarray import ( + difference, + intersection, + isdisjoint, + issubset, + issuperset, + symmetric_difference, + union, +) def register_accessors(): diff --git a/piso/accessor.py b/piso/accessor.py index cb03bf6..354c174 100644 --- a/piso/accessor.py +++ b/piso/accessor.py @@ -118,6 +118,29 @@ def symmetric_difference( return_type=return_type, ) + @Appender(docstrings.isdisjoint_docstring, join="\n", indents=1) + def isdisjoint(self, *interval_arrays): + return intervalarray.isdisjoint( + self._interval_array, + *interval_arrays, + ) + + @Appender(docstrings.issuperset_docstring, join="\n", indents=1) + def issuperset(self, *interval_arrays, squeeze=False): + return intervalarray.issuperset( + self._interval_array, + *interval_arrays, + squeeze=squeeze, + ) + + @Appender(docstrings.issubset_docstring, join="\n", indents=1) + def issubset(self, *interval_arrays, squeeze=False): + return intervalarray.issubset( + self._interval_array, + *interval_arrays, + squeeze=squeeze, + ) + def _register_accessors(): _register_accessor("piso", pd.IntervalIndex)(ArrayAccessor) diff --git a/piso/docstrings/accessor.py b/piso/docstrings/accessor.py index da9a779..dc6e5d8 100644 --- a/piso/docstrings/accessor.py +++ b/piso/docstrings/accessor.py @@ -257,6 +257,101 @@ """ +isdisjoint_examples = """ +Examples +----------- + +>>> import pandas as pd +>>> import piso +>>> piso.register_accessors() + +>>> arr1 = pd.arrays.IntervalArray.from_tuples( +... [(0, 3), (2, 4)], +... ) +>>> arr2 = pd.arrays.IntervalArray.from_tuples( +... [(4, 7), (8, 11)], +... ) +>>> arr3 = pd.arrays.IntervalArray.from_tuples( +... [(2, 4), (7, 8)], +... ) + +>>> arr1.piso.isdisjoint() +False + +>>> arr2.piso.isdisjoint() +True + +>>> arr1.piso.isdisjoint(arr2) +True + +>>> arr1.piso.isdisjoint(arr3) +False +""" + +issuperset_examples = """ +Examples +----------- + +>>> import pandas as pd +>>> import piso +>>> piso.register_accessors() + +>>> arr1 = pd.arrays.IntervalArray.from_tuples( +... [(0, 4), (3, 6), (7, 8), (10, 12)], +... ) +>>> arr2 = pd.arrays.IntervalArray.from_tuples( +... [(2, 5), (7, 8)], +... ) +>>> arr3 = pd.arrays.IntervalArray.from_tuples( +... [(3, 4), (10, 11)], +... ) + +>>> arr1.piso.issuperset(arr2) +True + +>>> arr1.piso.issuperset(arr2, squeeze=False) +array([ True]) + +>>> arr1.piso.issuperset(arr2, arr3) +array([ True, True]) + +>>> arr2.piso.issuperset(arr3) +False +""" + + +issubset_examples = """ +Examples +----------- + +>>> import pandas as pd +>>> import piso +>>> piso.register_accessors() + +>>> arr1 = pd.arrays.IntervalArray.from_tuples( +... [(2, 5), (7, 8)], +... ) +>>> arr2 = pd.arrays.IntervalArray.from_tuples( +... [(0, 4), (3, 6), (7, 8), (10, 12)], +... ) +>>> arr3 = pd.arrays.IntervalArray.from_tuples( +... [(3, 4), (10, 11)], +... ) + +>>> arr1.piso.issubset(arr2) +True + +>>> arr1.piso.issubset(arr2, squeeze=False) +array([ True]) + +>>> arr1.piso.issubset(arr2, arr3) +array([ True, False]) + +>>> arr1.piso.issubset(arr3) +False +""" + + def join_params(list_of_param_strings): return "".join(list_of_param_strings).replace("\n\n", "\n") @@ -266,7 +361,7 @@ def join_params(list_of_param_strings): May contain zero or more arguments. """ -param_optional_args_difference = """ +param_optional_args_min_one = """ *interval_arrays : argument list of :class:`pandas.IntervalIndex` or :class:`pandas.arrays.IntervalArray` Must contain at least one argument. """ @@ -280,8 +375,8 @@ def join_params(list_of_param_strings): """ param_squeeze = """ -squeeze : boolean, default True - If True, will try to coerce the return value to a pandas.Interval. +squeeze : boolean, default {default} + If True, will try to coerce the return value to a single pandas.Interval. If supplied, must be done so as a keyword argument. """ @@ -291,17 +386,15 @@ def join_params(list_of_param_strings): If supplied, must be done so as a keyword argument. """ - template_doc = """ -Performs a set {operation} operation. - What is considered a set is determined by the number of positional arguments used, that is, determined by the size of *interval_arrays*. -If *interval_arrays* is empty then the sets are considered to be the intervals contained in *interval_array*. +If *interval_arrays* is empty then the sets are considered to be the intervals contained in the array object the +accessor belongs to (an instance of :class:`pandas.IntervalIndex`, :class:`pandas.arrays.IntervalArray`). If *interval_arrays* is not empty then the sets are considered to be the elements in *interval_arrays*, in addition to the -interval array object the accessor belongs to (an instance of :class:`pandas.IntervalIndex`, :class:`pandas.arrays.IntervalArray`). +intervals in the array object the accessor belongs to. Each of these arrays is assumed to contain disjoint intervals (and satisfy the definition of a set). Any array containing overlaps between intervals will be mapped to one with disjoint intervals via a union operation. @@ -312,11 +405,18 @@ def join_params(list_of_param_strings): Returns ---------- -:class:`pandas.IntervalIndex` or :class:`pandas.arrays.IntervalArray` +{return_type} {examples} """ +operation_template_doc = ( + """ +Performs a set {operation} operation. +""" + + template_doc +) + doc_difference_template = """ Performs a set difference operation. @@ -331,9 +431,6 @@ def join_params(list_of_param_strings): and the union of the sets in *interval_arrays*. This is equivalent to iteratively applying a set difference operation with each array in *interval_arrays* as the second operand. -Each of these array operands is assumed to contain disjoint intervals (and satisfy the definition of a set). Any array containing -overlaps between intervals will be mapped to one with disjoint intervals via a union operation. - {extra_desc} Parameters ---------- @@ -346,18 +443,46 @@ def join_params(list_of_param_strings): {examples} """ +is_super_sub_set_template = """ +Indicates whether a set is a {operation} of one, or more, other sets. + +The array elements of *interval_arrays*, and the interval array object the accessor belongs to +(an instance of :class:`pandas.IntervalIndex`, :class:`pandas.arrays.IntervalArray`) are considered to be the sets over which +the operation is performed. Each of these arrays is assumed to contain disjoint intervals (and satisfy the definition of a set). +Any array containing overlaps between intervals will be mapped to one with disjoint intervals via a union operation. + +The list *interval_arrays* must contain at least one element. The {operation} comparison is iteratively applied between +the interval array the accessor belongs to, and each array in *interval_arrays*. When *interval_arrays* contains multiple +interval arrays, the return type will be a numpy array. If it contains one interval array then the result can be coerced to +a single boolean using the *squeeze* parameter. + +Parameters +---------- +{params} + +Returns +---------- +:class:`pandas.IntervalIndex` or :class:`pandas.arrays.IntervalArray` + +{examples} +""" + +array_return_type = ( + ":class:`pandas.IntervalIndex` or :class:`pandas.arrays.IntervalArray`" +) union_params = join_params( [ param_optional_args, - param_squeeze, + param_squeeze.format(default="False"), param_return_type, ] ) -union_docstring = template_doc.format( +union_docstring = operation_template_doc.format( operation="union", extra_desc="", params=union_params, + return_type=array_return_type, examples=union_examples, ) @@ -365,21 +490,22 @@ def join_params(list_of_param_strings): [ param_optional_args, param_min_overlaps, - param_squeeze, + param_squeeze.format(default="False"), param_return_type, ] ) -intersection_docstring = template_doc.format( +intersection_docstring = operation_template_doc.format( operation="intersection", extra_desc="", params=intersection_params, + return_type=array_return_type, examples=intersection_examples, ) difference_params = join_params( [ - param_optional_args_difference, - param_squeeze, + param_optional_args_min_one, + param_squeeze.format(default="False"), param_return_type, ] ) @@ -387,6 +513,7 @@ def join_params(list_of_param_strings): operation="difference", extra_desc="", params=difference_params, + return_type=array_return_type, examples=difference_examples, ) @@ -395,7 +522,7 @@ def join_params(list_of_param_strings): [ param_optional_args, param_min_overlaps, - param_squeeze, + param_squeeze.format(default="False"), param_return_type, ] ) @@ -404,9 +531,55 @@ def join_params(list_of_param_strings): The parameter *min_overlaps* in :meth:`piso.intersection`, which defines the minimum number of intervals in an overlap required to constitute an intersection, follows through to symmetric difference under this definition. """ -symmetric_difference_docstring = template_doc.format( +symmetric_difference_docstring = operation_template_doc.format( operation="symmetric difference", extra_desc=symmetric_difference_extra_desc, params=symmetric_difference_params, + return_type=array_return_type, examples=symmetric_difference_examples, ) + + +isdisjoint_doc = ( + """ +Indicates whether one, or more, sets are disjoint or not. +""" + + template_doc +) + +isdisjoint_params = join_params( + [ + param_optional_args, + ] +) +isdisjoint_docstring = isdisjoint_doc.format( + extra_desc="", + params=isdisjoint_params, + return_type="boolean", + examples=isdisjoint_examples, +) + + +issuperset_params = join_params( + [ + param_optional_args_min_one, + param_squeeze.format(default="True"), + ] +) +issuperset_docstring = is_super_sub_set_template.format( + operation="superset", + params=issuperset_params, + examples=issuperset_examples, +) + +issubset_params = join_params( + [ + param_optional_args_min_one, + param_squeeze.format(default="True"), + ] +) +issubset_docstring = is_super_sub_set_template.format( + operation="subset", + params=issubset_params, + examples=issubset_examples, +) diff --git a/piso/docstrings/interval.py b/piso/docstrings/interval.py index 301f6ec..f326b96 100644 --- a/piso/docstrings/interval.py +++ b/piso/docstrings/interval.py @@ -150,8 +150,77 @@ Length: 2, closed: right, dtype: interval[float64] """ +issuperset_examples = """ +Examples +----------- + +>>> import pandas as pd +>>> import piso.interval + +>>> piso.interval.issuperset( +... pd.Interval(1, 4), +... pd.Interval(2, 4), +... ) +True + +>>> piso.interval.issuperset( +... pd.Interval(1, 4), +... pd.Interval(0, 3), +... ) +False + +>>> piso.interval.issuperset( +... pd.Interval(1, 4), +... pd.Interval(2, 4), +... pd.Interval(0, 3), +... ) +array([ True, False]) + +>>> piso.interval.issuperset( +... pd.Interval(0, 3), +... pd.Interval(0, 3), +... squeeze=False +... ) +array([ True]) +""" + + +issubset_examples = """ +Examples +----------- + +>>> import pandas as pd +>>> import piso.interval + +>>> piso.interval.issubset( +... pd.Interval(2, 4), +... pd.Interval(1, 4), +... ) +True + +>>> piso.interval.issubset( +... pd.Interval(2, 4), +... pd.Interval(0, 3), +... ) +False + +>>> piso.interval.issubset( +... pd.Interval(2, 4), +... pd.Interval(1, 4), +... pd.Interval(0, 3), +... ) +array([ True, False]) + +>>> piso.interval.issubset( +... pd.Interval(1, 4), +... pd.Interval(1, 4), +... squeeze=False +... ) +array([ True]) +""" + template_doc = """ -Performs the {operation} of two pandas.Intervals +Performs the {operation} of two :class:`pandas.Interval` Parameters ---------- @@ -160,7 +229,7 @@ interval2 : pandas.Interval the second operand squeeze : boolean, default True - If True, will try to coerce the return value to a pandas.Interval + If True, will try to coerce the return value to a :class:`pandas.Interval` Returns ---------- @@ -169,6 +238,7 @@ {examples} """ + union_docstring = template_doc.format(operation="union", examples=union_examples) intersection_docstring = template_doc.format( operation="intersection", examples=intersection_examples @@ -179,3 +249,33 @@ symmetric_difference_docstring = template_doc.format( operation="symmetric difference", examples=symmetric_difference_examples ) + + +is_sub_super_doc = """ +Indicates whether one :class:`pandas.Interval` is a {operation} of one, or more, others. + +Parameters +---------- +interval : :class:`pandas.Interval` + An interval, against which all other intervals belonging to *intervals* are compared. +*intervals : argument list of :class:`pandas.Interval` + Must contain at least one argument. +squeeze : boolean, default True + If True, will try to coerce the return value to a single boolean + +Returns +---------- +boolean, or :class:`numpy.ndarray` of booleans + +{examples} +""" + +issuperset_docstring = is_sub_super_doc.format( + operation="superset", + examples=issuperset_examples, +) + +issubset_docstring = is_sub_super_doc.format( + operation="subset", + examples=issubset_examples, +) diff --git a/piso/docstrings/intervalarray.py b/piso/docstrings/intervalarray.py index 09c6371..92e5f36 100644 --- a/piso/docstrings/intervalarray.py +++ b/piso/docstrings/intervalarray.py @@ -241,6 +241,99 @@ """ +isdisjoint_examples = """ +Examples +----------- + +>>> import pandas as pd +>>> import piso + +>>> arr1 = pd.arrays.IntervalArray.from_tuples( +... [(0, 3), (2, 4)], +... ) +>>> arr2 = pd.arrays.IntervalArray.from_tuples( +... [(4, 7), (8, 11)], +... ) +>>> arr3 = pd.arrays.IntervalArray.from_tuples( +... [(2, 4), (7, 8)], +... ) + +>>> piso.isdisjoint(arr1) +False + +>>> piso.isdisjoint(arr2) +True + +>>> piso.isdisjoint(arr1, arr2) +True + +>>> piso.isdisjoint(arr1, arr3) +False +""" + + +issuperset_examples = """ +Examples +----------- + +>>> import pandas as pd +>>> import piso + +>>> arr1 = pd.arrays.IntervalArray.from_tuples( +... [(0, 4), (3, 6), (7, 8), (10, 12)], +... ) +>>> arr2 = pd.arrays.IntervalArray.from_tuples( +... [(2, 5), (7, 8)], +... ) +>>> arr3 = pd.arrays.IntervalArray.from_tuples( +... [(3, 4), (10, 11)], +... ) + +>>> piso.issuperset(arr1, arr2) +True + +>>> piso.issuperset(arr1, arr2, squeeze=False) +array([ True]) + +>>> piso.issuperset(arr1, arr2, arr3) +array([ True, True]) + +>>> piso.issuperset(arr2, arr3) +False +""" + + +issubset_examples = """ +Examples +----------- + +>>> import pandas as pd +>>> import piso + +>>> arr1 = pd.arrays.IntervalArray.from_tuples( +... [(2, 5), (7, 8)], +... ) +>>> arr2 = pd.arrays.IntervalArray.from_tuples( +... [(0, 4), (3, 6), (7, 8), (10, 12)], +... ) +>>> arr3 = pd.arrays.IntervalArray.from_tuples( +... [(3, 4), (10, 11)], +... ) + +>>> piso.issubset(arr1, arr2) +True + +>>> piso.issubset(arr1, arr2, squeeze=False) +array([ True]) + +>>> piso.issubset(arr1, arr2, arr3) +array([ True, False]) + +>>> piso.issubset(arr1, arr3) +False +""" + + def join_params(list_of_param_strings): return "".join(list_of_param_strings).replace("\n\n", "\n") @@ -250,12 +343,22 @@ def join_params(list_of_param_strings): The first (and possibly only) operand to the {operation} operation. """ +param_interval_array_non_optional = """ +interval_array : :class:`pandas.IntervalIndex` or :class:`pandas.arrays.IntervalArray` + The first operand to the {operation} operation. +""" + +param_interval_sub_super_set = """ +interval_array : :class:`pandas.IntervalIndex` or :class:`pandas.arrays.IntervalArray` + The first operand to which all others are compared operation. +""" + param_optional_args = """ *interval_arrays : argument list of :class:`pandas.IntervalIndex` or :class:`pandas.arrays.IntervalArray` May contain zero or more arguments. """ -param_optional_args_difference = """ +param_optional_args_min_one = """ *interval_arrays : argument list of :class:`pandas.IntervalIndex` or :class:`pandas.arrays.IntervalArray` Must contain at least one argument. """ @@ -269,8 +372,8 @@ def join_params(list_of_param_strings): """ param_squeeze = """ -squeeze : boolean, default True - If True, will try to coerce the return value to a pandas.Interval. +squeeze : boolean, default {default} + If True, will try to coerce the return value to a single pandas.Interval. If supplied, must be done so as a keyword argument. """ @@ -282,8 +385,6 @@ def join_params(list_of_param_strings): template_doc = """ -Performs a set {operation} operation. - What is considered a set is determined by the number of positional arguments used, that is, determined by the size of *interval_arrays*. @@ -300,11 +401,20 @@ def join_params(list_of_param_strings): Returns ---------- -:class:`pandas.IntervalIndex` or :class:`pandas.arrays.IntervalArray` +{return_type} {examples} """ + +operation_template_doc = ( + """ +Performs a set {operation} operation. +""" + + template_doc +) + + doc_difference_template = """ Performs a set difference operation. @@ -317,9 +427,6 @@ def join_params(list_of_param_strings): multiple elements then the result is the set difference between *interval_array* and the union of the sets in *interval_arrays*. This is equivalent to iteratively applying a set difference operation with each array in *interval_arrays* as the second operand. -Each of these array operands is assumed to contain disjoint intervals (and satisfy the definition of a set). Any array containing -overlaps between intervals will be mapped to one with disjoint intervals via a union operation. - {extra_desc} Parameters ---------- @@ -332,19 +439,48 @@ def join_params(list_of_param_strings): {examples} """ +doc_is_sub_super_set_template = """ +Indicates whether a set is a {operation} of one, or more, other sets. + +The argument *interval_array* and the array elements of *interval_arrays* are all considered to be the sets for the purposes +of this set method. Each of these arrays is assumed to contain disjoint intervals (and satisfy the definition of a set). +Any array containing overlaps between intervals will be mapped to one with disjoint intervals via a union operation. + +The list *interval_arrays* must contain at least one element. The {operation} comparison is iteratively applied between +*interval_array* and each array in *interval_arrays*. When *interval_arrays* contains multiple interval arrays, the return +type will be a numpy array. If it contains one interval array then the result can be coerced to a single boolean using the +*squeeze* parameter. + +{extra_desc} +Parameters +---------- +{params} + +Returns +---------- +boolean, or :class:`numpy.ndarray` of boolean + +{examples} +""" + + +array_return_type = ( + ":class:`pandas.IntervalIndex` or :class:`pandas.arrays.IntervalArray`" +) union_params = join_params( [ param_interval_array.format(operation="union"), param_optional_args, - param_squeeze, + param_squeeze.format(default="False"), param_return_type, ] ) -union_docstring = template_doc.format( +union_docstring = operation_template_doc.format( operation="union", extra_desc="", params=union_params, + return_type=array_return_type, examples=union_examples, ) @@ -353,22 +489,23 @@ def join_params(list_of_param_strings): param_interval_array.format(operation="intersection"), param_optional_args, param_min_overlaps, - param_squeeze, + param_squeeze.format(default="False"), param_return_type, ] ) -intersection_docstring = template_doc.format( +intersection_docstring = operation_template_doc.format( operation="intersection", extra_desc="", params=intersection_params, + return_type=array_return_type, examples=intersection_examples, ) difference_params = join_params( [ - param_interval_array.format(operation="difference"), - param_optional_args_difference, - param_squeeze, + param_interval_array_non_optional.format(operation="difference"), + param_optional_args_min_one, + param_squeeze.format(default="False"), param_return_type, ] ) @@ -376,6 +513,7 @@ def join_params(list_of_param_strings): operation="difference", extra_desc="", params=difference_params, + return_type=array_return_type, examples=difference_examples, ) @@ -385,7 +523,7 @@ def join_params(list_of_param_strings): param_interval_array.format(operation="symmetric difference"), param_optional_args, param_min_overlaps, - param_squeeze, + param_squeeze.format(default="False"), param_return_type, ] ) @@ -394,9 +532,61 @@ def join_params(list_of_param_strings): The parameter *min_overlaps* in :meth:`piso.intersection`, which defines the minimum number of intervals in an overlap required to constitute an intersection, follows through to symmetric difference under this definition. """ -symmetric_difference_docstring = template_doc.format( +symmetric_difference_docstring = operation_template_doc.format( operation="symmetric difference", extra_desc=symmetric_difference_extra_desc, params=symmetric_difference_params, + return_type=array_return_type, examples=symmetric_difference_examples, ) + + +isdisjoint_doc = ( + """ +Indicates whether one, or more, sets are disjoint or not. +""" + + template_doc +) + +isdisjoint_params = join_params( + [ + param_interval_array.format(operation="isdisjoint"), + param_optional_args, + ] +) +isdisjoint_docstring = isdisjoint_doc.format( + extra_desc="", + params=isdisjoint_params, + return_type="boolean", + examples=isdisjoint_examples, +) + + +issuperset_params = join_params( + [ + param_interval_sub_super_set, + param_optional_args_min_one, + param_squeeze.format(default="True"), + ] +) +issuperset_docstring = doc_is_sub_super_set_template.format( + operation="superset", + extra_desc="", + params=issuperset_params, + examples=issuperset_examples, +) + + +issubset_params = join_params( + [ + param_interval_sub_super_set, + param_optional_args_min_one, + param_squeeze.format(default="True"), + ] +) +issubset_docstring = doc_is_sub_super_set_template.format( + operation="subset", + extra_desc="", + params=issubset_params, + examples=issubset_examples, +) diff --git a/piso/interval.py b/piso/interval.py index 4ac94db..619be29 100644 --- a/piso/interval.py +++ b/piso/interval.py @@ -1,3 +1,4 @@ +import numpy as np import pandas as pd import piso.docstrings.interval as docstrings @@ -109,3 +110,31 @@ def symmetric_difference(interval1, interval2, squeeze=True): closed=interval1.closed, ) return result + + +def _make_is_sub_or_superset(which, docstring): + + left_bound_comparator = {"super": np.less_equal, "sub": np.greater_equal}[which] + right_bound_comparator = {"super": np.greater_equal, "sub": np.less_equal}[which] + + @Appender(docstring, join="\n", indents=1) + def func(interval, *intervals, squeeze=True): + assert intervals + lefts = np.array([i.left for i in intervals]) + rights = np.array([i.right for i in intervals]) + + result = np.logical_and( + left_bound_comparator(interval.left, lefts), + right_bound_comparator(interval.right, rights), + ) + + if len(result) == 1 and squeeze: + result = result[0] + + return result + + return func + + +issuperset = _make_is_sub_or_superset("super", docstrings.issuperset_docstring) +issubset = _make_is_sub_or_superset("sub", docstrings.issubset_docstring) diff --git a/piso/intervalarray.py b/piso/intervalarray.py index 400ccaa..3c635aa 100644 --- a/piso/intervalarray.py +++ b/piso/intervalarray.py @@ -1,3 +1,4 @@ +import numpy as np import pandas as pd import staircase as sc @@ -103,3 +104,49 @@ def symmetric_difference( if squeeze and len(result) == 1: result = result[0] return result + + +@Appender(docstrings.isdisjoint_docstring, join="\n", indents=1) +def isdisjoint(interval_array, *interval_arrays): + _validate_array_of_intervals_arrays(interval_array, *interval_arrays) + if interval_arrays: + stairs = _make_stairs(interval_array, *interval_arrays) + result = stairs.max() <= 1 + elif len(interval_array) == 0: + result = True + else: + arr = np.stack([interval_array.left.values, interval_array.right.values]) + arr = arr[arr[:, 0].argsort()] + result = np.all(arr[0, 1:] >= arr[1, :-1]) + return result + + +def _create_is_super_or_sub(which, docstring): + + comparator_func = {"superset": sc.Stairs.ge, "subset": sc.Stairs.le}[which] + + @Appender(docstring, join="\n", indents=1) + def func(interval_array, *interval_arrays, squeeze=True): + _validate_array_of_intervals_arrays(interval_array, *interval_arrays) + assert interval_arrays + stepfunction = _interval_x_to_stairs(interval_array).make_boolean() + + def _comp(ia): + return bool( + comparator_func( + stepfunction, + _interval_x_to_stairs(ia).make_boolean(), + ) + ) + + result = np.array([_comp(ia) for ia in interval_arrays]) + + if squeeze and len(result) == 1: + result = result[0] + return result + + return func + + +issuperset = _create_is_super_or_sub("superset", docstrings.issuperset_docstring) +issubset = _create_is_super_or_sub("subset", docstrings.issubset_docstring) diff --git a/poetry.lock b/poetry.lock index 1f95306..b1013b5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -196,7 +196,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.0.1" +version = "6.0.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -1221,7 +1221,7 @@ test = ["pytest"] [[package]] name = "staircase" -version = "2.0.3" +version = "2.0.4" description = "A data analysis package based on modelling and manipulation of mathematical step functions. Strongly aligned with pandas." category = "main" optional = false @@ -1407,7 +1407,7 @@ codecov = [] [metadata] lock-version = "1.1" python-versions = "^3.6.1" -content-hash = "0c02826b5a19e9649cfa35dc3509171da804c1c04f713da7ebaeb7846010e98f" +content-hash = "0a47702bf0b60eb123c87a8953108f3b390be1ada8017fe850f7af71b67e1836" [metadata.files] alabaster = [ @@ -1531,39 +1531,39 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:abe8207dfb8a61ded9cd830d26c1073c8218fc0ae17eb899cfe8ec0fafae6e22"}, - {file = "coverage-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83faa3692e8306b20293889714fdf573d10ef5efc5843bd7c7aea6971487bd6a"}, - {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f82a17f2a77958f3eef40ad385fc82d4c6ba9a77a51a174efe03ce75daebbc16"}, - {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b06f4f1729e2963281d9cd6e65e6976bf27b44d4c07ac5b47223ce45f822cec"}, - {file = "coverage-6.0.1-cp310-cp310-win32.whl", hash = "sha256:7600fac458f74c68b097379f76f3a6e3a630493fc7fc94b6508fedd9d498c194"}, - {file = "coverage-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:2c5f39d1556e75fc3c4fb071f9e7cfa618895a999a0de763a541d730775d0d5f"}, - {file = "coverage-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3edbb3ec580c73e5a264f5d04f30245bc98eff1a26765d46c5c65134f0a0e2f7"}, - {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea452a2d83964d08232ade470091015e7ab9b8f53acbec10f2210fbab4ce7e43"}, - {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1770d24f45f1f2daeae34cfa3b33fcb29702153544cd2ad40d58399dd4ff53b5"}, - {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ad7182a82843f9f85487f44567c8c688f16c906bdb8d0e44ae462aed61cb8f1b"}, - {file = "coverage-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:9d242a2434801ef5125330deddb4cddba8990c9a49b3dec99dca17dd7eefba5a"}, - {file = "coverage-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e66c50f0ab445fec920a9f084914ea1776a809e3016c3738519048195f851bbb"}, - {file = "coverage-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5b1ceacb86e0a9558061dcc6baae865ed25933ea57effea644f21657cdce19bc"}, - {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e15ab5afbee34abf716fece80ea33ea09a82e7450512f022723b1a82ec9a4e"}, - {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6873f3f954d3e3ab8b1881f4e5307cc19f70c9f931c41048d9f7e6fd946eabe7"}, - {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0898d6948b31df13391cd40568de8f35fa5901bc922c5ae05cf070587cb9c666"}, - {file = "coverage-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9c416ba03844608f45661a5b48dc59c6b5e89956efe388564dd138ca8caf540b"}, - {file = "coverage-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:66fe33e9e0df58675e08e83fe257f89e7f625e7633ea93d0872154e09cce2724"}, - {file = "coverage-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:353a50f123f0185cdb7a1e1e3e2cfb9d1fd7e293cfaf68eedaf5bd8e02e3ec32"}, - {file = "coverage-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b81a4e667c45b13658b84f9b8f1d32ef86d5405fabcbd181b76b9e51d295f397"}, - {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:17426808e8e0824f864876312d41961223bf5e503bf8f1f846735279a60ea345"}, - {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e11cca9eb5c9b3eaad899728ee2ce916138399ee8cbbccaadc1871fecb750827"}, - {file = "coverage-6.0.1-cp38-cp38-win32.whl", hash = "sha256:0a7e55cc9f7efa22d5cc9966276ec7a40a8803676f6ccbfdc06a486fba9aa9ee"}, - {file = "coverage-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:4eb9cd910ca8e243f930243a9940ea1a522e32435d15668445753d087c30ee12"}, - {file = "coverage-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83682b73785d2e078e0b5f63410b8125b122e1a22422640c57edd4011c950f3e"}, - {file = "coverage-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b45f89a8ef65c29195f8f28dbe215f44ccb29d934f3e862d2a5c12e38698a793"}, - {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73880a80fad0597eca43e213e5e1711bf6c0fcdb7eb6b01b3b17841ebe5a7f8d"}, - {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f398d38e6ebc2637863db1d7be3d4f9c5174e7d24bb3b0716cdb1f204669cbcf"}, - {file = "coverage-6.0.1-cp39-cp39-win32.whl", hash = "sha256:1864bdf9b2ccb43e724051bc23a1c558daf101ad4488ede1945f2a8be1facdad"}, - {file = "coverage-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:c9c413c4397d4cdc7ca89286158d240ce524f9667b52c9a64dd7e13d16cf8815"}, - {file = "coverage-6.0.1-pp36-none-any.whl", hash = "sha256:65da6e3e8325291f012921bbf71fea0a97824e1c573981871096aac6e2cf0ec5"}, - {file = "coverage-6.0.1-pp37-none-any.whl", hash = "sha256:07efe1fbd72e67df026ad5109bcd216acbbd4a29d5208b3dab61779bae6b7b26"}, - {file = "coverage-6.0.1.tar.gz", hash = "sha256:3490ff6dbf3f7accf0750136ed60ae1f487bccc1f097740e3b21262bc9c89854"}, + {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"}, + {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"}, + {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"}, + {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"}, + {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"}, + {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"}, + {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"}, + {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"}, + {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"}, + {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"}, + {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"}, + {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"}, + {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"}, + {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"}, + {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"}, + {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"}, + {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"}, + {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"}, ] cycler = [ {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"}, @@ -2218,8 +2218,8 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] staircase = [ - {file = "staircase-2.0.3-py3-none-any.whl", hash = "sha256:d856f8d37ef789309ca575053457b9b52f77244ac1a5338af811c9ede982da50"}, - {file = "staircase-2.0.3.tar.gz", hash = "sha256:e0640f4813574b742a80a054cce25a00895571f8f43e4b7a52a58a53944b7872"}, + {file = "staircase-2.0.4-py3-none-any.whl", hash = "sha256:c45291e8c91c3faf485790b483563580098c55f2c2f586ac7b41f05cbd27d8ac"}, + {file = "staircase-2.0.4.tar.gz", hash = "sha256:25e019e3d3d930153bd2c5381029198e865db0b0732fdda6f85550930ffee14c"}, ] terminado = [ {file = "terminado-0.12.1-py3-none-any.whl", hash = "sha256:09fdde344324a1c9c6e610ee4ca165c4bb7f5bbf982fceeeb38998a988ef8452"}, diff --git a/pyproject.toml b/pyproject.toml index ae52f38..7e8fbbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "poetry.masonry.api" [tool.poetry] name = "piso" -version = "0.1.0" +version = "0.2.0" description = "Pandas Interval Set Operations: methods for set operations for pandas' Interval, IntervalArray and IntervalIndex" readme = "README.md" authors = ["Riley Clement "] @@ -41,7 +41,7 @@ classifiers=[ [tool.poetry.dependencies] python = "^3.6.1" -staircase = "^2.0.3" +staircase = "^2.0.4" pandas = "^1" diff --git a/tests/test_interval.py b/tests/test_interval.py index b0fda74..2927308 100644 --- a/tests/test_interval.py +++ b/tests/test_interval.py @@ -1,3 +1,6 @@ +import operator + +import numpy as np import pandas as pd import pytest @@ -624,3 +627,47 @@ def test_symmetric_difference_closed_value_error(closed_values): ) with pytest.raises(ClosedValueError): piso_interval.symmetric_difference(*intervals) + + +@pytest.mark.parametrize( + "tuples, squeeze, expected", + [ + ([(1, 2), (1, 2)], True, True), + ([(1, 3), (0, 2)], True, False), + ([(1, 3), (1, 2), (0, 1)], True, np.array([True, False])), + ([(1, 2), (1, 2)], False, np.array([True])), + ([(1, 3), (0, 2)], False, np.array([False])), + ([(1, 3), (1, 2), (0, 1)], False, np.array([True, False])), + ], +) +@pytest.mark.parametrize( + "closed", + ["left", "right"], +) +def test_issuperset(tuples, squeeze, expected, closed): + intervals = [pd.Interval(*i, closed=closed) for i in tuples] + result = piso_interval.issuperset(*intervals, squeeze=squeeze) + equal_op = np.array_equal if isinstance(expected, np.ndarray) else operator.eq + assert equal_op(result, expected) + + +@pytest.mark.parametrize( + "tuples, squeeze, expected", + [ + ([(1, 2), (1, 2)], True, True), + ([(1, 3), (0, 2)], True, False), + ([(1, 3), (1, 4), (0, 1)], True, np.array([True, False])), + ([(1, 2), (1, 2)], False, np.array([True])), + ([(1, 3), (0, 2)], False, np.array([False])), + ([(1, 3), (1, 4), (0, 1)], False, np.array([True, False])), + ], +) +@pytest.mark.parametrize( + "closed", + ["left", "right"], +) +def test_issubset(tuples, squeeze, expected, closed): + intervals = [pd.Interval(*i, closed=closed) for i in tuples] + result = piso_interval.issubset(*intervals, squeeze=squeeze) + equal_op = np.array_equal if isinstance(expected, np.ndarray) else operator.eq + assert equal_op(result, expected) diff --git a/tests/test_multiple_interval_array.py b/tests/test_multiple_interval_array.py index 74ed169..24dc187 100644 --- a/tests/test_multiple_interval_array.py +++ b/tests/test_multiple_interval_array.py @@ -1,3 +1,6 @@ +import operator + +import numpy as np import pandas as pd import pytest @@ -14,6 +17,9 @@ def get_accessor_method(self, function): piso_intervalarray.intersection: self.piso.intersection, piso_intervalarray.difference: self.piso.difference, piso_intervalarray.symmetric_difference: self.piso.symmetric_difference, + piso_intervalarray.isdisjoint: self.piso.isdisjoint, + piso_intervalarray.issuperset: self.piso.issuperset, + piso_intervalarray.issubset: self.piso.issubset, }[function] @@ -23,6 +29,9 @@ def get_package_method(function): piso_intervalarray.intersection: piso.intersection, piso_intervalarray.symmetric_difference: piso.symmetric_difference, piso_intervalarray.difference: piso.difference, + piso_intervalarray.isdisjoint: piso.isdisjoint, + piso_intervalarray.issuperset: piso.issuperset, + piso_intervalarray.issubset: piso.issubset, }[function] @@ -429,3 +438,136 @@ def test_difference_4(closed, interval_index, return_type, how): expected, interval_index, ) + + +def map_to_dates(interval_array, date_type): + def make_date(x): + ts = pd.Timestamp(f"2021-10-{x}") + if date_type == "numpy": + return ts.to_numpy() + if date_type == "datetime": + return ts.to_pydatetime() + if date_type == "timedelta": + return ts - pd.Timestamp("2021-10-1") + return ts + + return interval_array.from_arrays( + interval_array.left.map(make_date), + interval_array.right.map(make_date), + ) + + +def make_ia_from_tuples(interval_index, tuples, closed): + klass = pd.IntervalIndex if interval_index else pd.arrays.IntervalArray + return klass.from_tuples(tuples, closed=closed) + + +@pytest.mark.parametrize( + "interval_index", + [True, False], +) +@pytest.mark.parametrize( + "tuples, expected", + [ + ([], True), + ([(1, 3)], True), + ([(3, 11)], False), + ([(1, 2), (2, 3)], True), + ([(1, 2), (1, 3)], True), + ([(1, 3), (7, 9)], False), + ([(1, 5), (6, 7)], False), + ([(1, 2), (6, 7), (9, 10)], False), + ], +) +@pytest.mark.parametrize( + "closed", + ["left", "right"], +) +@pytest.mark.parametrize( + "date_type", + ["timestamp", "numpy", "datetime", "timedelta", None], +) +@pytest.mark.parametrize( + "how", + ["supplied", "accessor", "package"], +) +def test_isdisjoint(interval_index, tuples, expected, closed, date_type, how): + # all intervals are compared to ia3 + ia3 = make_ia3(interval_index, closed) # intervals = (3,4), (8,11) + ia3 = map_to_dates(ia3, date_type) + interval_array = make_ia_from_tuples(interval_index, tuples, closed) + interval_array = map_to_dates(interval_array, date_type) + result = perform_op( + ia3, interval_array, how=how, function=piso_intervalarray.isdisjoint + ) + assert result == expected + + +@pytest.mark.parametrize( + "interval_index", + [True, False], +) +@pytest.mark.parametrize( + "ia_makers, squeeze, expected", + [ + ([make_ia1, make_ia2], True, True), + ([make_ia1, make_ia3], True, False), + ([make_ia1, make_ia2, make_ia3], True, np.array([True, False])), + ([make_ia1, make_ia2], False, np.array([True])), + ([make_ia1, make_ia3], False, np.array([False])), + ([make_ia1, make_ia2, make_ia3], False, np.array([True, False])), + ], +) +@pytest.mark.parametrize( + "closed", + ["left", "right"], +) +@pytest.mark.parametrize( + "how", + ["supplied", "accessor", "package"], +) +def test_issuperset(interval_index, ia_makers, squeeze, expected, closed, how): + ias = [make_ia(interval_index, closed) for make_ia in ia_makers] + result = perform_op( + *ias, + how=how, + function=piso_intervalarray.issuperset, + squeeze=squeeze, + ) + equal_op = np.array_equal if isinstance(expected, np.ndarray) else operator.eq + assert equal_op(result, expected) + + +@pytest.mark.parametrize( + "interval_index", + [True, False], +) +@pytest.mark.parametrize( + "ia_makers, squeeze, expected", + [ + ([make_ia2, make_ia1], True, True), + ([make_ia3, make_ia1], True, False), + ([make_ia2, make_ia1, make_ia3], True, np.array([True, False])), + ([make_ia2, make_ia1], False, np.array([True])), + ([make_ia3, make_ia1], False, np.array([False])), + ([make_ia2, make_ia1, make_ia3], False, np.array([True, False])), + ], +) +@pytest.mark.parametrize( + "closed", + ["left", "right"], +) +@pytest.mark.parametrize( + "how", + ["supplied", "accessor", "package"], +) +def test_issubset(interval_index, ia_makers, squeeze, expected, closed, how): + ias = [make_ia(interval_index, closed) for make_ia in ia_makers] + result = perform_op( + *ias, + how=how, + function=piso_intervalarray.issubset, + squeeze=squeeze, + ) + equal_op = np.array_equal if isinstance(expected, np.ndarray) else operator.eq + assert equal_op(result, expected) diff --git a/tests/test_single_interval_array.py b/tests/test_single_interval_array.py index 3d7875e..bdb91dd 100644 --- a/tests/test_single_interval_array.py +++ b/tests/test_single_interval_array.py @@ -13,6 +13,9 @@ def get_accessor_method(self, function): piso_intervalarray.union: self.piso.union, piso_intervalarray.intersection: self.piso.intersection, piso_intervalarray.symmetric_difference: self.piso.symmetric_difference, + piso_intervalarray.isdisjoint: self.piso.isdisjoint, + piso_intervalarray.issuperset: self.piso.issuperset, + piso_intervalarray.issubset: self.piso.issubset, }[function] @@ -21,6 +24,9 @@ def get_package_method(function): piso_intervalarray.union: piso.union, piso_intervalarray.intersection: piso.intersection, piso_intervalarray.symmetric_difference: piso.symmetric_difference, + piso_intervalarray.isdisjoint: piso_intervalarray.isdisjoint, + piso_intervalarray.issuperset: piso.issuperset, + piso_intervalarray.issubset: piso.issubset, }[function] @@ -65,6 +71,11 @@ def make_ia3(interval_index, closed): return ia3 +def make_ia_from_tuples(interval_index, tuples, closed): + klass = pd.IntervalIndex if interval_index else pd.arrays.IntervalArray + return klass.from_tuples(tuples, closed=closed) + + def assert_interval_array_equal(interval_array, expected, interval_index): if interval_index: interval_array = interval_array.values @@ -418,3 +429,58 @@ def test_symmetric_difference_min_overlaps_all_2( expected, interval_index, ) + + +def map_to_dates(interval_array, date_type): + def make_date(x): + ts = pd.Timestamp(f"2021-10-{x}") + if date_type == "numpy": + return ts.to_numpy() + if date_type == "datetime": + return ts.to_pydatetime() + if date_type == "timedelta": + return ts - pd.Timestamp("2021-10-1") + return ts + + return interval_array.from_arrays( + interval_array.left.map(make_date), + interval_array.right.map(make_date), + ) + + +@pytest.mark.parametrize( + "interval_index", + [True, False], +) +@pytest.mark.parametrize( + "tuples, expected", + [ + ([], True), + ([(1, 2), (2, 3)], True), + ([(1, 2), (3, 4)], True), + ([(1, 3), (2, 4)], False), + ([(1, 4), (2, 3)], False), + ([(1, 2), (2, 3), (3, 4)], True), + ([(1, 2), (3, 4), (5, 6)], True), + ([(1, 3), (2, 4), (5, 6)], False), + ([(1, 4), (2, 3), (5, 6)], False), + ], +) +@pytest.mark.parametrize( + "closed", + ["left", "right"], +) +@pytest.mark.parametrize( + "date_type", + ["timestamp", "numpy", "datetime", "timedelta", None], +) +@pytest.mark.parametrize( + "how", + ["supplied", "accessor", "package"], +) +def test_isdisjoint(interval_index, tuples, expected, closed, date_type, how): + + interval_array = make_ia_from_tuples(interval_index, tuples, closed) + interval_array = map_to_dates(interval_array, date_type) + result = perform_op(interval_array, how=how, function=piso_intervalarray.isdisjoint) + assert result == expected