Skip to content

Commit de88d07

Browse files
authored
fix: Use ISO year for weekly cohort grouping to fix year boundary bug (#86)
1 parent 48ad9f8 commit de88d07

5 files changed

Lines changed: 40 additions & 14 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ assert MonthEvents('active', now.year, now.month).has_events_marked() == True
7878
How many users have been active this week?:
7979

8080
```python
81-
print(len(WeekEvents('active', now.year, now.isocalendar()[1])))
81+
iso_year, iso_week, _ = now.isocalendar()
82+
print(len(WeekEvents('active', iso_year, iso_week)))
8283
```
8384

8485
Iterate over all users active this week:
@@ -111,7 +112,8 @@ MonthEvents('active').from_date(now) == MonthEvents('active', now.year, now.mont
111112
Get the list of these users (user ids):
112113

113114
```python
114-
print(list(WeekEvents('active', now.year, now.isocalendar()[1])))
115+
iso_year, iso_week, _ = now.isocalendar()
116+
print(list(WeekEvents('active', iso_year, iso_week)))
115117
```
116118

117119
There are special methods `prev` and `next` returning "sibling" events and

bitmapist/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
4747
How many users have been active this week?::
4848
49-
print len(WeekEvents('active', now.year, now.isocalendar()[1]))
49+
iso_year, iso_week, _ = now.isocalendar()
50+
print len(WeekEvents('active', iso_year, iso_week))
5051
5152
Perform bit operations. Which users that have been active last month are still active this month?::
5253

bitmapist/cohort/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,8 @@ def _day_events_fn(key: str, date: date, system: str):
384384

385385
def _weeks_events_fn(key: str, date: date, system: str):
386386
cls = WeekEvents
387-
cls_args = (date.year, date.isocalendar()[1], system)
387+
iso_year, iso_week, _ = date.isocalendar()
388+
cls_args = (iso_year, iso_week, system)
388389
return _dispatch(key, cls, cls_args)
389390

390391

test/test_bitmapist.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ def test_mark_with_diff_days():
2626
assert 124 not in MonthEvents("active", now.year, now.month)
2727

2828
# Week
29-
assert 123 in WeekEvents("active", now.year, now.isocalendar()[1])
30-
assert 124 not in WeekEvents("active", now.year, now.isocalendar()[1])
29+
iso_year, iso_week, _ = now.isocalendar()
30+
assert 123 in WeekEvents("active", iso_year, iso_week)
31+
assert 124 not in WeekEvents("active", iso_year, iso_week)
3132

3233
# Day
3334
assert 123 in DayEvents("active", now.year, now.month, now.day)

test/test_cohort.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from datetime import datetime, timedelta, timezone
1+
from datetime import date, datetime, timedelta, timezone
22

33
import pytest
44

55
from bitmapist import mark_event
6-
from bitmapist.cohort import get_dates_data
6+
from bitmapist.cohort import _weeks_events_fn, get_dates_data
77

88

99
@pytest.fixture
@@ -35,22 +35,16 @@ def events():
3535
("active", None, "active", None, [2, 100, 50]),
3636
("active", None, "unknown", None, [2, "", ""]),
3737
("unknown", None, "active", None, [0, "", ""]),
38-
3938
# Tests with select1b (AND conditions for select1)
4039
("signup", "active", "active", None, [2, 100, 50]),
41-
4240
# Tests with both select1b and select2b
4341
("task1", "task2", "task2", "task1", [2, 100, 100]),
44-
4542
# When select1 has no events but select1b has events, result should be 0
4643
("unknown", "active", "active", None, [0, "", ""]),
47-
4844
# When select1 has events but select2 AND select2b results in 0
4945
("active", None, "unknown", "active", [2, "", ""]),
50-
5146
# When select1 has events but select1b has no events (no overlap)
5247
("active", "unknown", "active", None, [0, "", ""]),
53-
5448
# When select2 has events but select2b has no events (no overlap)
5549
("active", None, "active", "unknown", [2, "", ""]),
5650
],
@@ -67,3 +61,30 @@ def test_cohort(select1, select1b, select2, select2b, expected, events):
6761
num_of_rows=1,
6862
)
6963
assert r[0][1:] == expected
64+
65+
66+
def test_weeks_events_fn_iso_year_boundary():
67+
"""Test that _weeks_events_fn uses ISO year, not calendar year.
68+
69+
At year boundaries, a date's calendar year can differ from its ISO year.
70+
For example, Dec 30, 2024 is in ISO week 1 of 2025.
71+
"""
72+
# Dec 30, 2024 is a Monday in ISO week 1 of 2025
73+
dec_30_2024 = date(2024, 12, 30)
74+
iso_year, iso_week, _ = dec_30_2024.isocalendar()
75+
assert iso_year == 2025 # Verify our test date assumption
76+
assert iso_week == 1
77+
78+
event = _weeks_events_fn("test_event", dec_30_2024, "default")
79+
# Should use ISO year (2025), not calendar year (2024)
80+
assert event.year == 2025
81+
assert event.week == 1
82+
83+
# Jan 1, 2025 is also in ISO week 1 of 2025
84+
jan_1_2025 = date(2025, 1, 1)
85+
event2 = _weeks_events_fn("test_event", jan_1_2025, "default")
86+
assert event2.year == 2025
87+
assert event2.week == 1
88+
89+
# Both dates should produce the same week event
90+
assert event.redis_key == event2.redis_key

0 commit comments

Comments
 (0)