Skip to content

feat: Support typed RDATE/EXDATE recurrence - #116

Open
sirambd wants to merge 9 commits into
mainfrom
p10-handle-exdate-rdate
Open

feat: Support typed RDATE/EXDATE recurrence#116
sirambd wants to merge 9 commits into
mainfrom
p10-handle-exdate-rdate

Conversation

@sirambd

@sirambd sirambd commented Aug 10, 2026

Copy link
Copy Markdown
Member

This pull request extends the event recurrence support in the calendar core by introducing explicit handling and storage for RDATE and EXDATE fields, which represent additional and excluded recurrence dates. It updates the data model, database schema, and related mapping logic to support these fields, and refactors how recurrence is detected and queried, moving from relying solely on rrule to a more general hasRecurrence flag. The changes also improve how recurrence bounds are calculated, ensuring correct handling of events with only RDATEs or a mix of RRULE and RDATE.

Recurrence Model and Database Changes:

  • Added rDates and exDates fields (lists of IcalDateValue) and a hasRecurrence boolean to EventEntity, and updated all related projections and database queries to use these fields instead of relying solely on rrule. [1] [2] [3] [4] [5] [6]
  • Added new type converters in CalendarTypeConverters to serialize/deserialize lists of IcalDateValue for database storage.
  • Bumped the database schema version to 13 to accommodate these new fields.

Recurrence Logic and Mapping Improvements:

  • Refactored recurrence bounds calculation to support events with only RDATEs, or both RRULE and RDATEs, by introducing new logic in toRecurrenceBoundsEntity and related helper methods. This ensures correct calculation of first/last occurrences and series bounds. [1] [2]
  • Updated the event-to-domain mapping functions to pass rDates and exDates to the domain model, ensuring the recurrence logic in the application layer receives all relevant data. [1] [2]

Other Notable Updates:

  • Updated documentation and SQL queries in EventDao to use hasRecurrence instead of rrule IS NOT NULL, making recurrence detection more robust. [1] [2] [3]
  • Added necessary imports for IcalDateValue throughout the codebase to support the new fields and logic. [1] [2] [3] [4] [5]

These changes make the recurrence system more flexible and standards-compliant, allowing for more accurate event representation and querying, especially for complex recurrence scenarios.

Copilot AI balanced review requested due to automatic review settings August 10, 2026 15:28
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds typed RDATE/EXDATE recurrence support across CalDAV parsing, persistence, querying, mapping, expansion, and tests.

Changes:

  • Parses and stores typed recurrence dates with timezone metadata.
  • Expands RRULE/RDATE sets while applying EXDATE exclusions.
  • Adds recurrence-aware database bounds, queries, schema, and tests.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
CalendarKmpDav/.../RustCaldavBridge.kt Maps UniFFI recurrence dates.
CalendarKmpDav/.../RemoteIcalDateValue.kt Defines remote typed date values.
CalendarKmpDav/.../RemoteDavEvent.kt Adds RDATE/EXDATE fields.
CalendarKmpDav/rust/.../models.rs Adds Rust recurrence date models.
CalendarKmpDav/rust/.../events.rs Parses RDATE/EXDATE properties.
CalendarCore/.../RecurringEventExpansionTest.kt Tests inclusion and exclusion.
CalendarCore/.../RemoteDavEventToEntityRecurrenceTest.kt Tests recurrence-date parsing.
CalendarCore/.../RecurrenceRuleEntitySerializationTest.kt Tests typed-value serialization.
CalendarCore/.../EventDaoTest.kt Tests RDATE range queries.
CalendarCore/.../RecurringEventExpansion.kt Implements recurrence-set expansion.
CalendarCore/.../RecurrenceRuleFailureReason.kt Adds unsupported-period reason.
CalendarCore/.../IcalDateValue.kt Defines domain recurrence values.
CalendarCore/.../EventTiming.kt Carries RDATE/EXDATE values.
CalendarCore/.../EventRepository.kt Passes recurrence dates to expansion.
CalendarCore/.../CalendarRepository.kt Resolves recurrence during sync.
CalendarCore/.../RemoteDavEventToEntity.kt Maps and validates recurrence values.
CalendarCore/.../RecurrenceRuleToBoundsEntity.kt Computes combined recurrence bounds.
CalendarCore/.../EventTimingEntityToDomain.kt Maps recurrence dates to domain.
CalendarCore/.../EventEntityToDomain.kt Propagates stored recurrence dates.
CalendarCore/.../EventCalendarColorInRange.kt Extends lightweight projection.
CalendarCore/.../EventEntity.kt Persists recurrence fields and flag.
CalendarCore/.../DatabaseProvider.kt Bumps database version.
CalendarCore/.../EventDao.kt Queries using recurrence-set bounds.
CalendarCore/.../CalendarTypeConverters.kt Serializes typed date lists.
CalendarCore/schemas/.../13.json Defines schema version 13.
Suppressed comments (2)

CalendarCore/src/commonMain/kotlin/com/infomaniak/multiplatform_calendar/core/data/mapper/RecurrenceRuleToBoundsEntity.kt:168

  • For floating events, an RDATE can precede DTSTART, but this branch stores only the upper local bound. RECURRING_FLOATING still uses event.dtStart as its lower bound, so a query around that earlier RDATE filters out the master before expansion. Persist/query an earliest local occurrence bound (or conservatively omit the floating lower-bound check).
        base.copy(
            firstOccurrenceInstantMs = null,
            lastOccurrenceEndLocalDateTime = maxOf(
                base.lastOccurrenceEndLocalDateTime ?: timing.dtEndEffective,
                localEnds.maxOrNull() ?: timing.dtEndEffective,
            ),

CalendarCore/src/commonMain/kotlin/com/infomaniak/multiplatform_calendar/core/data/mapper/RecurrenceRuleToBoundsEntity.kt:183

  • For an all-day RDATE-only series, lowBoundInstantMs() has already subtracted 14 hours while durationMs has added 14 hours, so adding them cancels the required upper padding for the included DTSTART. If DTSTART is the latest member, range queries in negative-offset zones can drop its tail. Compute this end from the unpadded dtStartInstantMs.
            val dtStartMs = timing.lowBoundInstantMs()
            val lastBound = maxNotNull(dtStartMs?.let { it + durationMs }, anchoredEnds.maxOrNull())

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

val rDates: List<IcalDateValue> = emptyList(),
val exDates: List<IcalDateValue> = emptyList(),
) {
val hasRecurrence: Boolean get() = rule != null || rDates.isNotEmpty()
Comment on lines +131 to +134
dateValue.startInstantMs(timing)?.let { startMs ->
firstAnchoredStartInstantMs = minNotNull(firstAnchoredStartInstantMs, startMs)
anchoredEnds += startMs + durationMs
}
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.

2 participants