Skip to content

Recurrent events for bookingslots and opening hours#97

Draft
jathvl wants to merge 1 commit into
mainfrom
feat/calendar-algorithm
Draft

Recurrent events for bookingslots and opening hours#97
jathvl wants to merge 1 commit into
mainfrom
feat/calendar-algorithm

Conversation

@jathvl
Copy link
Copy Markdown
Collaborator

@jathvl jathvl commented May 3, 2026

This is the algorithmic part

Initial library implementation largely implemented with the use of an LLM (Claude)

Closes #78, closes #55, closes #58

Co-authored-by: Claude <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an internal iCalendar-style recurrence engine intended to support generating recurring booking slots/opening hours and modeling recurring holidays in the backend domain.

Changes:

  • Introduces app.lib.ical implementing RRULE expansion plus EXDATE/RDATE handling and basic RRULE (de)serialization.
  • Adds app.lib.norwegian_holidays with fixed-date and Easter-relative Norwegian public holidays.
  • Adds a comprehensive unit test suite for the recurrence engine (tests/test_ical.py).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
backend/app/lib/ical.py New RRULE/RDATE/EXDATE recurrence implementation with serialization helpers.
backend/app/lib/norwegian_holidays.py New holiday definitions (fixed + Easter-relative) built on the recurrence primitives.
backend/tests/test_ical.py Unit tests covering many recurrence expansion scenarios, including DST behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +3 to +22
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

import pytest

from app.lib.ical import (
FR,
MO,
SA,
SU,
TH,
TU,
WE,
EventOccurrence,
Frequency,
RecurrenceRule,
RecurrentEvent,
Weekday,
)

Comment thread backend/app/lib/ical.py
Comment on lines +112 to +118
u = (
self.until.astimezone(ZoneInfo("UTC"))
if self.until.tzinfo
else self.until
)
parts.append(f"UNTIL={u.strftime('%Y%m%dT%H%M%SZ')}")
if self.wkst != 0:
Comment thread backend/app/lib/ical.py
Comment on lines +152 to +161
kwargs: dict[str, Any] = {"freq": Frequency(parsed["FREQ"])}
if "INTERVAL" in parsed:
kwargs["interval"] = int(parsed["INTERVAL"])
if "COUNT" in parsed:
kwargs["count"] = int(parsed["COUNT"])
if "UNTIL" in parsed:
kwargs["until"] = _parse_ical_datetime(parsed["UNTIL"])
if "WKST" in parsed:
kwargs["wkst"] = _WEEKDAY_NAMES.index(parsed["WKST"])

Comment thread backend/app/lib/ical.py
Comment on lines +228 to +233
for dt in candidates:
if dt < dtstart:
continue
if self.until is not None and dt > self.until:
return
yield dt
Comment on lines +24 to +34
def _yearly(summary: str, month: int, day: int) -> RecurrentEvent:
"""An all-day, fixed-date holiday that recurs every year."""
return RecurrentEvent(
summary=summary,
dtstart=datetime(1970, month, day),
duration=timedelta(days=1),
rrule=RecurrenceRule(
freq=Frequency.YEARLY,
bymonth=[month],
bymonthday=[day],
),
Comment on lines +86 to +108
def _easter_relative(
summary: str,
offset_days: int,
start_year: int = 1970,
end_year: int = 2100,
) -> RecurrentEvent:
"""A holiday whose date each year is ``Easter Sunday + offset_days``.

Materialized as explicit RDATEs over ``[start_year, end_year]``.
"""
occurrences = [
datetime.combine(
easter_sunday(y) + timedelta(days=offset_days),
time(0),
)
for y in range(start_year, end_year + 1)
]
return RecurrentEvent(
summary=summary,
dtstart=occurrences[0],
duration=timedelta(days=1),
rdates=occurrences[1:],
)
Comment on lines +67 to +83
def easter_sunday(year: int) -> date:
"""Date of Easter Sunday in the given (Gregorian) year.

Uses the anonymous Gregorian algorithm (Meeus / Jones / Butcher).
"""
a = year % 19
b, c = divmod(year, 100)
d, e = divmod(b, 4)
f = (b + 8) // 25
g = (b - f + 1) // 3
h = (19 * a + b - d - g + 15) % 30
i, k = divmod(c, 4)
L = (32 + 2 * e + 2 * i - h - k) % 7
m = (a + 11 * h + 22 * L) // 451
month = (h + L - 7 * m + 114) // 31
day = ((h + L - 7 * m + 114) % 31) + 1
return date(year, month, day)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backend: opening hours and booking slot generation #57 Admin functionallity: backend Validate the domain model

2 participants