From ca9b2ac0b27295aa363cb87f0517486aee096a08 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Mon, 27 Apr 2026 11:04:30 -0700 Subject: [PATCH] feat: add is_nth_weekday() and parse_occurrence() for scheduled comment gating --- app/services/platforms/reddit_comment.py | 45 ++++++++++++++++ .../services/platforms/test_reddit_comment.py | 51 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 app/services/platforms/reddit_comment.py create mode 100644 tests/services/platforms/test_reddit_comment.py diff --git a/app/services/platforms/reddit_comment.py b/app/services/platforms/reddit_comment.py new file mode 100644 index 0000000..ce11058 --- /dev/null +++ b/app/services/platforms/reddit_comment.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import logging +from datetime import date, timedelta + +import httpx + +from app.services.platforms.base import PostingStrategy, PostResult + +logger = logging.getLogger(__name__) + +# Weekday names → int (0=Mon, 6=Sun) +_WEEKDAY_MAP = { + "monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3, + "friday": 4, "saturday": 5, "sunday": 6, +} +# Ordinal names → n +_ORDINAL_MAP = {"first": 1, "second": 2, "third": 3} + + +def is_nth_weekday(dt: date, weekday: int, n: int) -> bool: + """True if dt is the nth occurrence of weekday (0=Mon, 6=Sun) in its month.""" + first_of_month = dt.replace(day=1) + days_until = (weekday - first_of_month.weekday()) % 7 + first_occurrence = first_of_month + timedelta(days=days_until) + nth_occurrence = first_occurrence + timedelta(weeks=n - 1) + return dt == nth_occurrence + + +def parse_occurrence(occurrence: str | None) -> tuple[int, int] | None: + """Parse an occurrence string into (weekday, n) or None for 'every'. + + Supported: "first_sunday", "second_monday", "third_friday", etc. + Returns None for "every" or None input. + Raises ValueError for unrecognised patterns. + """ + if occurrence is None or occurrence == "every": + return None + parts = occurrence.lower().split("_", 1) + if len(parts) != 2: + raise ValueError(f"Unrecognised occurrence format: {occurrence!r}") + ordinal, weekday_name = parts + if ordinal not in _ORDINAL_MAP or weekday_name not in _WEEKDAY_MAP: + raise ValueError(f"Unrecognised occurrence: {occurrence!r}") + return _WEEKDAY_MAP[weekday_name], _ORDINAL_MAP[ordinal] diff --git a/tests/services/platforms/test_reddit_comment.py b/tests/services/platforms/test_reddit_comment.py new file mode 100644 index 0000000..a13bb5d --- /dev/null +++ b/tests/services/platforms/test_reddit_comment.py @@ -0,0 +1,51 @@ +from datetime import date +from app.services.platforms.reddit_comment import is_nth_weekday, parse_occurrence + + +# --- is_nth_weekday --- + +def test_first_sunday_of_april_2026(): + # 2026-04-05 is the first Sunday of April 2026 + assert is_nth_weekday(date(2026, 4, 5), weekday=6, n=1) is True + + +def test_second_sunday_of_april_2026_is_not_first(): + assert is_nth_weekday(date(2026, 4, 12), weekday=6, n=1) is False + + +def test_first_sunday_of_may_2026(): + # 2026-05-03 is the first Sunday of May 2026 + assert is_nth_weekday(date(2026, 5, 3), weekday=6, n=1) is True + + +def test_last_friday_not_matched_by_first(): + # 2026-04-24 is the last Friday of April — not the first + assert is_nth_weekday(date(2026, 4, 24), weekday=4, n=1) is False + + +def test_first_friday_of_april_2026(): + # 2026-04-03 is the first Friday + assert is_nth_weekday(date(2026, 4, 3), weekday=4, n=1) is True + + +# --- parse_occurrence --- + +def test_parse_occurrence_every_returns_none(): + assert parse_occurrence("every") is None + + +def test_parse_occurrence_none_returns_none(): + assert parse_occurrence(None) is None + + +def test_parse_occurrence_first_sunday(): + weekday, n = parse_occurrence("first_sunday") + assert weekday == 6 and n == 1 + + +def test_parse_occurrence_unknown_raises(): + try: + parse_occurrence("fourth_wednesday") + assert False, "Expected ValueError" + except ValueError: + pass