Repository: ibis-project/ibis

Issue:
BUG: Add max_lookback to window replace and combine functions
Rows with max lookback adds a new attribute to Window, but was not accounted for in window functions that return a new Window by either combining two windows' attributes or replacing some existing attributes.


Discussion:

Resolving patch:
diff --git a/ibis/expr/window.py b/ibis/expr/window.py
--- a/ibis/expr/window.py
+++ b/ibis/expr/window.py
@@ -88,6 +88,7 @@ def __init__(
         order_by=None,
         preceding=None,
         following=None,
+        max_lookback=None,
         how='rows',
     ):
         if group_by is None:
@@ -111,7 +112,7 @@ def __init__(
             self.max_lookback = preceding.max_lookback
         else:
             self.preceding = _sequence_to_tuple(preceding)
-            self.max_lookback = None
+            self.max_lookback = max_lookback
 
         self.following = _sequence_to_tuple(following)
         self.how = how
@@ -227,9 +228,11 @@ def combine(self, window):
                     "Expecting '{}' Window, got '{}'"
                 ).format(self.how.upper(), window.how.upper())
             )
+        mlb = self.max_lookback
         kwds = dict(
             preceding=self.preceding or window.preceding,
             following=self.following or window.following,
+            max_lookback=mlb if mlb is not None else window.max_lookback,
             group_by=self._group_by + window._group_by,
             order_by=self._order_by + window._order_by,
         )
@@ -245,6 +248,7 @@ def _replace(self, **kwds):
             order_by=kwds.get('order_by', self._order_by),
             preceding=kwds.get('preceding', self.preceding),
             following=kwds.get('following', self.following),
+            max_lookback=kwds.get('max_lookback', self.max_lookback),
             how=kwds.get('how', self.how),
         )
         return Window(**new_kwds)
@@ -284,7 +288,11 @@ def equals(self, other, cache=None):
 
         equal = ops.all_equal(
             self.preceding, other.preceding, cache=cache
-        ) and ops.all_equal(self.following, other.following, cache=cache)
+        ) and ops.all_equal(
+            self.following, other.following, cache=cache
+        ) and ops.all_equal(
+            self.max_lookback, other.max_lookback, cache=cache
+        )
         cache[self, other] = equal
         return equal
 


Test patch:
diff --git a/ibis/expr/tests/test_window_functions.py b/ibis/expr/tests/test_window_functions.py
--- a/ibis/expr/tests/test_window_functions.py
+++ b/ibis/expr/tests/test_window_functions.py
@@ -63,6 +63,55 @@ def test_combine_windows(alltypes):
     with pytest.raises(ibis.common.IbisInputError):
         w1.combine(w6)
 
+    w7 = ibis.trailing_window(
+        rows_with_max_lookback(3, ibis.interval(days=5))
+    )
+    w8 = ibis.trailing_window(
+        rows_with_max_lookback(5, ibis.interval(days=7))
+    )
+    w9 = w7.combine(w8)
+    expected = ibis.trailing_window(
+        rows_with_max_lookback(3, ibis.interval(days=5))
+    )
+    assert_equal(w9, expected)
+
+
+def test_replace_window(alltypes):
+    t = alltypes
+    w1 = ibis.window(
+        preceding=5,
+        following=1,
+        group_by=t.a,
+        order_by=t.b
+    )
+    w2 = w1.group_by(t.c)
+    expected = ibis.window(
+        preceding=5,
+        following=1,
+        group_by=[t.a, t.c],
+        order_by=t.b
+    )
+    assert_equal(w2, expected)
+
+    w3 = w1.order_by(t.d)
+    expected = ibis.window(
+        preceding=5,
+        following=1,
+        group_by=t.a,
+        order_by=[t.b, t.d]
+    )
+    assert_equal(w3, expected)
+
+    w4 = ibis.trailing_window(
+        rows_with_max_lookback(3, ibis.interval(months=3))
+    )
+    w5 = w4.group_by(t.a)
+    expected = ibis.trailing_window(
+        rows_with_max_lookback(3, ibis.interval(months=3)),
+        group_by=t.a
+    )
+    assert_equal(w5, expected)
+
 
 def test_over_auto_bind(alltypes):
     # GH #542
@@ -174,9 +223,64 @@ def test_preceding_following_validate(alltypes):
             case()
 
 
-@pytest.mark.xfail(raises=AssertionError, reason='NYT')
 def test_window_equals(alltypes):
-    assert False
+    t = alltypes
+    w1 = ibis.window(
+        preceding=1,
+        following=2,
+        group_by=t.a,
+        order_by=t.b
+    )
+    w2 = ibis.window(
+        preceding=1,
+        following=2,
+        group_by=t.a,
+        order_by=t.b
+    )
+    assert w1.equals(w2)
+
+    w3 = ibis.window(
+        preceding=1,
+        following=2,
+        group_by=t.a,
+        order_by=t.c
+    )
+    assert not w1.equals(w3)
+
+    w4 = ibis.range_window(
+        preceding=ibis.interval(hours=3),
+        group_by=t.d
+    )
+    w5 = ibis.range_window(
+        preceding=ibis.interval(hours=3),
+        group_by=t.d
+    )
+    assert w4.equals(w5)
+
+    w6 = ibis.range_window(
+        preceding=ibis.interval(hours=1),
+        group_by=t.d
+    )
+    assert not w4.equals(w6)
+
+    w7 = ibis.trailing_window(
+        rows_with_max_lookback(3, ibis.interval(days=5)),
+        group_by=t.a,
+        order_by=t.b
+    )
+    w8 = ibis.trailing_window(
+        rows_with_max_lookback(3, ibis.interval(days=5)),
+        group_by=t.a,
+        order_by=t.b
+    )
+    assert w7.equals(w8)
+
+    w9 = ibis.trailing_window(
+        rows_with_max_lookback(3, ibis.interval(months=5)),
+        group_by=t.a,
+        order_by=t.b
+    )
+    assert not w7.equals(w9)
 
 
 def test_determine_how():