Repository: pola-rs/polars (language: python)

Issue:
bug: `group_by_dynamic` `period` and `every` get applied in reverse order
### Checks

- [X] I have checked that this issue has not already been reported.
- [X] I have confirmed this bug exists on the [latest version](https://pypi.org/project/polars/) of Polars.

### Reproducible example

```python
In [18]: df = pl.DataFrame({'a': [datetime(2020, 1, 1), datetime(2020, 2, 1), datetime(2020, 3, 1)], 'b': [1,2,3]})

In [19]: df.group_by_dynamic('a', every='1mo', period='45d', include_boundaries=True).agg(pl.col('b').sum())
Out[19]:
shape: (3, 4)
┌─────────────────────┬─────────────────────┬─────────────────────┬─────┐
│ _lower_boundary     ┆ _upper_boundary     ┆ a                   ┆ b   │
│ ---                 ┆ ---                 ┆ ---                 ┆ --- │
│ datetime[μs]        ┆ datetime[μs]        ┆ datetime[μs]        ┆ i64 │
╞═════════════════════╪═════════════════════╪═════════════════════╪═════╡
│ 2020-01-01 00:00:00 ┆ 2020-02-15 00:00:00 ┆ 2020-01-01 00:00:00 ┆ 3   │
│ 2020-02-01 00:00:00 ┆ 2020-03-15 00:00:00 ┆ 2020-02-01 00:00:00 ┆ 5   │
│ 2020-03-01 00:00:00 ┆ 2020-04-15 00:00:00 ┆ 2020-03-01 00:00:00 ┆ 3   │
└─────────────────────┴─────────────────────┴─────────────────────┴─────┘
```


### Log output

_No response_

### Issue description

It looks like `period` is getting applied before `every`, as opposed to
```
[start + every, start + every + period)
```
which is what's documented

I think the documentation expresses the desired behaviour - if you pass `period=45d`, then you expect each period to be 45 days long

### Expected behavior

```
shape: (3, 4)
┌─────────────────────┬─────────────────────┬─────────────────────┬─────┐
│ _lower_boundary     ┆ _upper_boundary     ┆ a                   ┆ b   │
│ ---                 ┆ ---                 ┆ ---                 ┆ --- │
│ datetime[μs]        ┆ datetime[μs]        ┆ datetime[μs]        ┆ i64 │
╞═════════════════════╪═════════════════════╪═════════════════════╪═════╡
│ 2020-01-01 00:00:00 ┆ 2020-02-15 00:00:00 ┆ 2020-01-01 00:00:00 ┆ 3   │
│ 2020-02-01 00:00:00 ┆ 2022-03-18 00:00:00 ┆ 2020-02-01 00:00:00 ┆ 5   │
│ 2020-03-01 00:00:00 ┆ 2022-04-15 00:00:00 ┆ 2020-03-01 00:00:00 ┆ 3   │
└─────────────────────┴─────────────────────┴─────────────────────┴─────┘
```

### Installed versions

<details>

```
--------Version info---------
Polars:              1.12.0
Index type:          UInt32
Platform:            Linux-5.15.153.1-microsoft-standard-WSL2-x86_64-with-glibc2.35
Python:              3.12.5 (main, Aug 14 2024, 05:08:31) [Clang 18.1.8 ]
LTS CPU:             False

----Optional dependencies----
adbc_driver_manager  <not installed>
altair               5.4.1
cloudpickle          3.0.0
connectorx           <not installed>
deltalake            <not installed>
fastexcel            <not installed>
fsspec               2024.9.0
gevent               <not installed>
great_tables         0.11.0
matplotlib           3.9.2
nest_asyncio         1.6.0
numpy                2.1.3
openpyxl             <not installed>
pandas               2.2.3
pyarrow              18.0.0
pydantic             <not installed>
pyiceberg            <not installed>
sqlalchemy           <not installed>
torch                <not installed>
xlsx2csv             <not installed>
xlsxwriter           <not installed>
```

</details>



Discussion / hints:

Fix patch:
diff --git a/crates/polars-time/src/windows/window.rs b/crates/polars-time/src/windows/window.rs
index c7a29b846c58..9609f2abc514 100644
--- a/crates/polars-time/src/windows/window.rs
+++ b/crates/polars-time/src/windows/window.rs
@@ -327,15 +327,15 @@ impl Iterator for BoundsIter<'_> {
                 // Issue is that `next` needs to return `Option`.
                 TimeUnit::Nanoseconds => {
                     self.bi.start = self.window.every.add_ns(self.bi.start, self.tz).unwrap();
-                    self.bi.stop = self.window.every.add_ns(self.bi.stop, self.tz).unwrap();
+                    self.bi.stop = self.window.period.add_ns(self.bi.start, self.tz).unwrap();
                 },
                 TimeUnit::Microseconds => {
                     self.bi.start = self.window.every.add_us(self.bi.start, self.tz).unwrap();
-                    self.bi.stop = self.window.every.add_us(self.bi.stop, self.tz).unwrap();
+                    self.bi.stop = self.window.period.add_us(self.bi.start, self.tz).unwrap();
                 },
                 TimeUnit::Milliseconds => {
                     self.bi.start = self.window.every.add_ms(self.bi.start, self.tz).unwrap();
-                    self.bi.stop = self.window.every.add_ms(self.bi.stop, self.tz).unwrap();
+                    self.bi.stop = self.window.period.add_ms(self.bi.start, self.tz).unwrap();
                 },
             }
             Some(out)


Test patch:
diff --git a/py-polars/tests/unit/operations/test_group_by_dynamic.py b/py-polars/tests/unit/operations/test_group_by_dynamic.py
index 0c083e204725..0b4cc1ec2531 100644
--- a/py-polars/tests/unit/operations/test_group_by_dynamic.py
+++ b/py-polars/tests/unit/operations/test_group_by_dynamic.py
@@ -1043,3 +1043,36 @@ def test_group_by_dynamic_exclude_index_from_expansion_17075() -> None:
         "n": [0, 2, 4, 6],
         "m": [0, 2, 4, 6],
     }
+
+
+def test_group_by_dynamic_overlapping_19704() -> None:
+    df = pl.DataFrame(
+        {
+            "a": [datetime(2020, 1, 1), datetime(2020, 2, 1), datetime(2020, 3, 1)],
+            "b": [1, 2, 3],
+        }
+    )
+    result = df.group_by_dynamic(
+        "a", every="1mo", period="45d", include_boundaries=True
+    ).agg(pl.col("b").sum())
+    expected = pl.DataFrame(
+        {
+            "_lower_boundary": [
+                datetime(2020, 1, 1, 0, 0),
+                datetime(2020, 2, 1, 0, 0),
+                datetime(2020, 3, 1, 0, 0),
+            ],
+            "_upper_boundary": [
+                datetime(2020, 2, 15, 0, 0),
+                datetime(2020, 3, 17, 0, 0),
+                datetime(2020, 4, 15, 0, 0),
+            ],
+            "a": [
+                datetime(2020, 1, 1, 0, 0),
+                datetime(2020, 2, 1, 0, 0),
+                datetime(2020, 3, 1, 0, 0),
+            ],
+            "b": [3, 5, 3],
+        }
+    )
+    assert_frame_equal(result, expected)