Skip to content

fix: complete UNTIL/DTSTART value type normalization for rrule-temporal 1.4.2+#433

Merged
jens-maus merged 3 commits intojens-maus:masterfrom
KristjanESPERANTO:until
Jan 31, 2026
Merged

fix: complete UNTIL/DTSTART value type normalization for rrule-temporal 1.4.2+#433
jens-maus merged 3 commits intojens-maus:masterfrom
KristjanESPERANTO:until

Conversation

@KristjanESPERANTO
Copy link
Contributor

@KristjanESPERANTO KristjanESPERANTO commented Jan 28, 2026

PR #432 added initial support for VALUE=DATE preservation, but some edge cases still failed with rrule-temporal 1.4.2+ strict RFC5545 validation.

Additional fixes:

  • Strip time component from UNTIL when DTSTART is DATE-only
  • Ensure UNTIL has UTC 'Z' suffix for DATE-TIME events
  • Convert non-UTC UNTIL to UTC when DTSTART has TZID
  • Embed DTSTART;VALUE=DATE in rruleString for proper validation

Note: Since rrule-temporal is an active project, we unfortunately have to play catch-up with the changes, but I think that the advantages outweigh this and the migration to rrule-temporal was the right decision.

Summary by CodeRabbit

  • Bug Fixes

    • Recurring events now correctly distinguish date-only vs date-time DTSTART, normalizing UNTIL consistently.
    • Improved timezone handling with safer fallbacks and consistent UTC formatting for recurrence rules.
    • RRULE construction now reliably includes matching DTSTART for date-only events so recurrences are generated consistently.
  • Tests

    • Added tests for DATE-only RRULE UNTIL behavior and TZID handling regression.
  • Chores

    • Updated rrule-temporal dependency.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

Reworked ical.js RRULE/DTSTART/UNTIL handling: detect DATE-only vs DATE-TIME DTSTART, emit DTSTART;VALUE=DATE for date-only, normalize UNTIL to date-only or UTC datetimes with TZID-aware Temporal resolution and fallbacks, wrap rrule-temporal usage in a compatibility layer, and bump rrule-temporal dependency.

Changes

Cohort / File(s) Summary
RRULE / DTSTART / UNTIL logic
ical.js
Reworked RRULE preprocessing to filter TZID fragments, detect DATE-only vs DATE-TIME DTSTART, inject DTSTART;VALUE=DATE for date-only events, normalize UNTIL to date-only or UTC datetime depending on DTSTART semantics, use TZID-aware Temporal.ZonedDateTime construction via tzutil/Intl with UTC fallbacks, and wrap rrule-temporal usage in an RRuleCompatWrapper.
Dependency
package.json
Bumped rrule-temporal from ^1.3.0 to ^1.4.5.
Tests
test/date-only-rrule-until.test.js, test/advanced.test.js
Added date-only UNTIL consistency test (date-only-rrule-until.test.js) and a TZID/rrule string regression test under advanced.test.js verifying DTSTART;TZID= presence and tzid in rrule.options without embedding TZID in the RRULE line.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Parser as Parser / ical.js
participant TZ as TZ Resolver (tzutil / Intl)
participant Temporal as Temporal.ZonedDateTime
participant RRule as rrule-temporal
participant Compat as RRuleCompatWrapper
Parser->>Parser: parse DTSTART and RRULE lines
alt DTSTART is DATE-only
Parser->>RRule: build rule string with "DTSTART;VALUE=DATE" + RRULE
RRule-->>Compat: create Temporal-backed rrule
else DTSTART is DATE-TIME
Parser->>TZ: resolve TZID or determine UTC
TZ-->>Parser: tz info or error fallback
Parser->>Temporal: construct ZonedDateTime (or UTC fallback)
Temporal-->>RRule: supply DTSTART for Temporal rrule creation
RRule-->>Compat: create compatibility wrapper
end
Compat-->>Parser: expose rrule-compatible instance for recurrence expansion

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Poem

🐰 I nibble DTSTART, chase UNTIL through the night,

I wrap Temporal rules and set TZs aright.
Date hops, UTC drops, recurrences align,
I tidy the rules — a carrot-coded sign.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: completing UNTIL/DTSTART value type normalization for rrule-temporal 1.4.2+. It directly relates to the core changeset focus on UNTIL/DTSTART handling and timezone normalization across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@ical.js`:
- Around line 839-843: The DTSTART line is built using UTC getters which shifts
date-only Date objects created with local components; update the code that
builds dtstartString (currently using
curr.start.getUTCFullYear/getUTCMonth/getUTCDate) to use local getters
(getFullYear, getMonth + 1, getDate) so it matches the getDateKey pattern and
the dateParameter logic (also check any UNTIL validation that assumes local
components); locate the dtstartString construction and the
dateParameter/getDateKey functions to make the change consistently.

…al 1.4.2+

PR jens-maus#432 added initial support for VALUE=DATE preservation, but some
edge cases still failed with rrule-temporal 1.4.2+ strict RFC5545
validation.

Additional fixes:
- Strip time component from UNTIL when DTSTART is DATE-only
- Ensure UNTIL has UTC 'Z' suffix for DATE-TIME events
- Convert non-UTC UNTIL to UTC when DTSTART has TZID
- Embed DTSTART;VALUE=DATE in rruleString for proper validation
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ical.js (1)

789-791: Strip TZID fragments when removing inline DTSTART.

When rule contains DTSTART;TZID=..., splitting on ; produces a standalone TZID=... segment that is not filtered out by the current logic. This segment is not valid RRULE syntax and can break rrule-temporal parsing. The filter must also exclude segments starting with TZID=.

🛠️ Proposed fix
          let rruleOnly = rule.split(';')
            .filter(segment => !segment.startsWith('DTSTART') && !segment.startsWith('VALUE='))
            .join(';');
+          let rruleOnly = rule.split(';')
+            .filter(segment =>
+              !segment.startsWith('DTSTART')
+              && !segment.startsWith('VALUE=')
+              && !segment.startsWith('TZID=')
+            )
+            .join(';');

When the internal rule string contains DTSTART;TZID=..., splitting on
semicolons produces an orphaned TZID=... segment. This segment is now
filtered out along with DTSTART and VALUE= for cleaner RRULE syntax.

Note: rrule-temporal tolerates these extra segments, so this is a
code quality improvement rather than a bug fix.
@jens-maus jens-maus merged commit c8181d8 into jens-maus:master Jan 31, 2026
16 checks passed
@KristjanESPERANTO KristjanESPERANTO deleted the until branch January 31, 2026 22:50
KristjanESPERANTO added a commit to KristjanESPERANTO/node-ical that referenced this pull request Jan 31, 2026
Google Calendar sometimes produces invalid RRULE entries where UNTIL
is a DATE-only value (YYYYMMDD) even though DTSTART is a DATE-TIME.

Examples from real Google Calendars:
- DTSTART;TZID=Europe/Oslo:20211216T180000
  RRULE:FREQ=WEEKLY;UNTIL=20211216  (missing time!)

- DTSTART:20110106T000000Z
  RRULE:FREQ=YEARLY;UNTIL=20361231  (missing time!)

This violates RFC5545 which requires UNTIL to have the same value type
as DTSTART. The previous fix in jens-maus#433 only handled the opposite case
(DTSTART=DATE with UNTIL having time).

Solution:
When DTSTART is DATE-TIME but UNTIL is DATE-only, interpret UNTIL as
end-of-day (23:59:59) in the event's timezone, then convert to UTC.
This correctly handles edge cases like Pacific/Auckland (UTC+13) where
a naive T235959Z interpretation would incorrectly include events from
the next day.

Falls back to T235959Z (UTC) when no timezone info is available.

Fixes jens-maus#435
Related: ggaabe/rrule-temporal#104
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