Repository: mars-project/mars

Issue:
Support `Series.between`
Support `Series.between`


Discussion:
Hi, How can I help with this issue?

> Hi, How can I help with this issue?

You're welcome.

Resolving patch:
diff --git a/mars/dataframe/core.py b/mars/dataframe/core.py
--- a/mars/dataframe/core.py
+++ b/mars/dataframe/core.py
@@ -15,6 +15,7 @@
 # limitations under the License.
 
 import weakref
+import warnings
 from collections.abc import Iterable
 from io import StringIO
 from typing import Union, Dict, Any
@@ -1268,6 +1269,99 @@ def to_frame(self, name=None):
         name = name or self.name or 0
         return dataframe_from_tensor(self, columns=[name])
 
+    def between(self, left, right, inclusive="both"):
+        """
+        Return boolean Series equivalent to left <= series <= right.
+        This function returns a boolean vector containing `True` wherever the
+        corresponding Series element is between the boundary values `left` and
+        `right`. NA values are treated as `False`.
+
+        Parameters
+        ----------
+        left : scalar or list-like
+            Left boundary.
+        right : scalar or list-like
+            Right boundary.
+        inclusive : {"both", "neither", "left", "right"}
+            Include boundaries. Whether to set each bound as closed or open.
+
+        Returns
+        -------
+        Series
+            Series representing whether each element is between left and
+            right (inclusive).
+
+        See Also
+        --------
+        Series.gt : Greater than of series and other.
+        Series.lt : Less than of series and other.
+
+        Notes
+        -----
+        This function is equivalent to ``(left <= ser) & (ser <= right)``
+
+        Examples
+        --------
+        >>> import mars.dataframe as md
+        >>> s = md.Series([2, 0, 4, 8, np.nan])
+        Boundary values are included by default:
+        >>> s.between(1, 4).execute()
+        0     True
+        1    False
+        2     True
+        3    False
+        4    False
+        dtype: bool
+
+        With `inclusive` set to ``"neither"`` boundary values are excluded:
+        >>> s.between(1, 4, inclusive="neither").execute()
+        0     True
+        1    False
+        2    False
+        3    False
+        4    False
+        dtype: bool
+
+        `left` and `right` can be any scalar value:
+        >>> s = md.Series(['Alice', 'Bob', 'Carol', 'Eve'])
+        >>> s.between('Anna', 'Daniel').execute()
+        0    False
+        1     True
+        2     True
+        3    False
+        dtype: bool
+        """
+        if inclusive is True or inclusive is False:
+            warnings.warn(
+                "Boolean inputs to the `inclusive` argument are deprecated in"
+                "favour of `both` or `neither`.",
+                FutureWarning,
+                stacklevel=2,
+            )
+            if inclusive:
+                inclusive = "both"
+            else:
+                inclusive = "neither"
+        if inclusive == "both":
+            lmask = self >= left
+            rmask = self <= right
+        elif inclusive == "left":
+            lmask = self >= left
+            rmask = self < right
+        elif inclusive == "right":
+            lmask = self > left
+            rmask = self <= right
+        elif inclusive == "neither":
+            lmask = self > left
+            rmask = self < right
+        else:
+            raise ValueError(
+                "Inclusive has to be either string of 'both',"
+                "'left', 'right', or 'neither'."
+            )
+
+        return lmask & rmask
+
 
 class BaseDataFrameChunkData(ChunkData):
     __slots__ = '_dtypes_value',


Test patch:
diff --git a/mars/dataframe/tests/test_core.py b/mars/dataframe/tests/test_core.py
--- a/mars/dataframe/tests/test_core.py
+++ b/mars/dataframe/tests/test_core.py
@@ -254,3 +254,88 @@ def test_key_value(setup):
 
     result = idx.values.execute().fetch()
     np.testing.assert_array_equal(result, raw.values)
+
+
+def test_between(setup):
+    pd_series = pd.Series(pd.date_range("1/1/2000", periods=10))
+    pd_left, pd_right = pd_series[2], pd_series[7]
+    expected = pd_series.between(pd_left, pd_right)
+
+    series = Series(pd_series, chunk_size=5)
+    left, right = series[2], series[7]
+    result = series.between(left, right).execute().fetch()
+    pd.testing.assert_series_equal(result, expected)
+
+
+def test_between_datetime_values(setup):
+    pd_series = pd.Series(pd.bdate_range("1/1/2000", periods=20).astype(object))
+    pd_series[::2] = np.nan
+    expected = pd_series[3:18].dropna()
+
+    series = Series(pd_series, chunk_size=5)
+    result = series[series.between(series[3], series[17])]
+    result = result.execute().fetch()
+    pd.testing.assert_series_equal(result, expected)
+
+    result = series[series.between(series[3], series[17], inclusive="neither")]
+    result = result.execute().fetch()
+    expected = pd_series[5:16].dropna()
+    pd.testing.assert_series_equal(result, expected)
+
+
+def test_between_period_values(setup):
+    pd_series = pd.Series(pd.period_range("2000-01-01", periods=10, freq="D"))
+    pd_left, pd_right = pd_series[2], pd_series[7]
+    expected = pd_series.between(pd_left, pd_right)
+    series = Series(pd_series, chunk_size=5)
+    left, right = series[2], series[7]
+    result = series.between(left, right).execute().fetch()
+    pd.testing.assert_series_equal(result, expected)
+
+
+def test_between_inclusive_string(setup):
+    pd_series = pd.Series(pd.date_range("1/1/2000", periods=10))
+    pd_left, pd_right = pd_series[2], pd_series[7]
+    expected = pd_series.between(pd_left, pd_right)
+
+    series = Series(pd_series, chunk_size=5)
+    left, right = series[2], series[7]
+    result = series.between(left, right, inclusive="both").execute().fetch()
+    pd.testing.assert_series_equal(result, expected)
+
+    result = series.between(left, right, inclusive="left").execute().fetch()
+    expected = pd_series.between(pd_left, pd_right, inclusive="left")
+    pd.testing.assert_series_equal(result, expected)
+
+    result = series.between(left, right, inclusive="right").execute().fetch()
+    expected = pd_series.between(pd_left, pd_right, inclusive="right")
+    pd.testing.assert_series_equal(result, expected)
+
+    result = series.between(left, right, inclusive="neither").execute().fetch()
+    expected = pd_series.between(pd_left, pd_right, inclusive="neither")
+    pd.testing.assert_series_equal(result, expected)
+
+
+def test_between_error_args(setup):
+    series = Series(pd.date_range("1/1/2000", periods=10), chunk_size=5)
+    left, right = series[2], series[7]
+
+    value_error_msg = (
+        "Inclusive has to be either string of 'both',"
+        "'left', 'right', or 'neither'."
+    )
+
+    with pytest.raises(ValueError, match=value_error_msg):
+        series = Series(pd.date_range("1/1/2000", periods=10), chunk_size=5)
+        series.between(left, right, inclusive="yes").execute().fetch()
+
+
+def test_between_inclusive_warning(setup):
+    pd_series = pd.Series(pd.date_range("1/1/2000", periods=10))
+    series = Series(pd_series, chunk_size=5)
+    left, right = series[2], series[7]
+
+    with pytest.warns(FutureWarning):
+        series.between(left, right, inclusive=False)
+    with pytest.warns(FutureWarning):
+        series.between(left, right, inclusive=True)