Claude Code Plugins

Community-maintained marketplace

Feedback

RRULE pattern parsing and next occurrence calculation for recurring tasks. Uses python-dateutil for RFC 5545 RRULE support, simplified patterns (daily, weekly, monthly, yearly), and UTC-only time handling.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name rrule-recurring-tasks
description RRULE pattern parsing and next occurrence calculation for recurring tasks. Uses python-dateutil for RFC 5545 RRULE support, simplified patterns (daily, weekly, monthly, yearly), and UTC-only time handling.

RRULE Recurring Tasks Skill

RRULE pattern parsing and next occurrence calculation for recurring task management.

Quick Start

Installation

pip install python-dateutil

1. Basic RRULE Patterns

Simplified Patterns

from dateutil.rrule import rrule, DAILY, WEEKLY, MONTHLY, YEARLY
from dateutil.parser import parse
from datetime import datetime

# Daily
daily_rule = rrule(DAILY, dtstart=parse("2025-12-29T10:00:00Z"), count=10)

# Weekly
weekly_rule = rrule(WEEKLY, dtstart=parse("2025-12-29T10:00:00Z"), count=10)

# Monthly
monthly_rule = rrule(MONTHLY, dtstart=parse("2025-12-29T10:00:00Z"), count=10)

# Yearly
yearly_rule = rrule(YEARLY, dtstart=parse("2025-12-29T10:00:00Z"), count=10)

Parse RRULE String

from dateutil.rrule import rrulestr

# Parse RFC 5545 RRULE string
rrule_string = """
DTSTART:20251229T100000Z
RRULE:FREQ=DAILY;INTERVAL=1;COUNT=10
"""

rule = rrulestr(rrule_string)
occurrences = list(rule)

2. Next Occurrence Calculation

Calculate Next Occurrence

from dateutil.rrule import rrule, DAILY, WEEKLY, MONTHLY, YEARLY
from dateutil.parser import parse
from datetime import datetime, timezone

def calculate_next_occurrence(
    recurring_pattern: str,
    current_occurrence: datetime,
    recurring_end_date: datetime | None = None
) -> datetime | None:
    """Calculate next occurrence from recurring pattern."""
    # Parse pattern
    if recurring_pattern.startswith("FREQ="):
        # Full RRULE string
        rule = rrulestr(
            f"DTSTART:{current_occurrence.strftime('%Y%m%dT%H%M%SZ')}\n"
            f"RRULE:{recurring_pattern}"
        )
    else:
        # Simplified pattern (DAILY, WEEKLY, MONTHLY, YEARLY)
        freq_map = {
            "DAILY": DAILY,
            "WEEKLY": WEEKLY,
            "MONTHLY": MONTHLY,
            "YEARLY": YEARLY
        }
        freq = freq_map.get(recurring_pattern.upper())
        if not freq:
            return None
        
        rule = rrule(
            freq,
            dtstart=current_occurrence,
            count=1,
            interval=1
        )
    
    # Get next occurrence
    next_occurrences = list(rule)
    if not next_occurrences:
        return None
    
    next_occurrence = next_occurrences[0]
    
    # Check if past end date
    if recurring_end_date and next_occurrence > recurring_end_date:
        return None
    
    return next_occurrence.replace(tzinfo=timezone.utc)

Example Usage

from datetime import datetime, timezone

# Current occurrence (task completed)
current = datetime(2025, 12, 29, 10, 0, 0, tzinfo=timezone.utc)

# Calculate next daily occurrence
next_occurrence = calculate_next_occurrence(
    recurring_pattern="DAILY",
    current_occurrence=current
)
# Result: 2025-12-30T10:00:00Z

# With end date
end_date = datetime(2026, 1, 31, 23, 59, 59, tzinfo=timezone.utc)
next_occurrence = calculate_next_occurrence(
    recurring_pattern="WEEKLY",
    current_occurrence=current,
    recurring_end_date=end_date
)

3. RRULE Parser Service

Complete Parser

from dateutil.rrule import rrule, rrulestr, DAILY, WEEKLY, MONTHLY, YEARLY
from dateutil.parser import parse
from datetime import datetime, timezone
from typing import Literal

class RRuleParser:
    """Parse RRULE patterns and calculate next occurrences."""
    
    SIMPLIFIED_PATTERNS = {
        "DAILY": DAILY,
        "WEEKLY": WEEKLY,
        "MONTHLY": MONTHLY,
        "YEARLY": YEARLY
    }
    
    def parse_pattern(self, pattern: str) -> rrule:
        """Parse RRULE pattern (simplified or full RFC 5545)."""
        pattern_upper = pattern.upper()
        
        # Check if simplified pattern
        if pattern_upper in self.SIMPLIFIED_PATTERNS:
            freq = self.SIMPLIFIED_PATTERNS[pattern_upper]
            return rrule(freq, interval=1)
        
        # Full RRULE string
        if pattern.startswith("FREQ="):
            return rrulestr(f"RRULE:{pattern}")
        
        # Try parsing as full RRULE
        try:
            return rrulestr(pattern)
        except Exception:
            raise ValueError(f"Invalid RRULE pattern: {pattern}")
    
    def calculate_next(
        self,
        pattern: str,
        dtstart: datetime,
        end_date: datetime | None = None
    ) -> datetime | None:
        """Calculate next occurrence from pattern."""
        rule = self.parse_pattern(pattern)
        
        # Set dtstart
        rule.dtstart = dtstart.replace(tzinfo=timezone.utc)
        
        # Get next occurrence
        occurrences = list(rule)
        if not occurrences:
            return None
        
        next_occurrence = occurrences[0]
        
        # Check end date
        if end_date and next_occurrence > end_date.replace(tzinfo=timezone.utc):
            return None
        
        return next_occurrence
    
    def validate_pattern(self, pattern: str) -> bool:
        """Validate RRULE pattern."""
        try:
            self.parse_pattern(pattern)
            return True
        except Exception:
            return False

4. Simplified Pattern Generation

Generate Simplified Patterns

def generate_simplified_pattern(
    frequency: Literal["DAILY", "WEEKLY", "MONTHLY", "YEARLY"],
    interval: int = 1,
    byweekday: str | None = None
) -> str:
    """Generate simplified RRULE pattern."""
    if frequency == "DAILY":
        if interval == 1:
            return "DAILY"
        return f"FREQ=DAILY;INTERVAL={interval}"
    
    if frequency == "WEEKLY":
        pattern = f"FREQ=WEEKLY;INTERVAL={interval}"
        if byweekday:
            pattern += f";BYDAY={byweekday}"
        return pattern
    
    if frequency == "MONTHLY":
        return f"FREQ=MONTHLY;INTERVAL={interval}"
    
    if frequency == "YEARLY":
        return f"FREQ=YEARLY;INTERVAL={interval}"
    
    raise ValueError(f"Unknown frequency: {frequency}")

5. UTC-Only Time Handling

Always Use UTC

from datetime import datetime, timezone

# ✅ Good: UTC timestamps
current = datetime.utcnow().replace(tzinfo=timezone.utc)
next_occurrence = calculate_next_occurrence("DAILY", current)

# ❌ Bad: Timezone-aware without UTC
current = datetime.now()  # Local timezone

Convert to UTC

from dateutil.parser import parse

# Parse and convert to UTC
dt = parse("2025-12-29T10:00:00-05:00")  # EST
dt_utc = dt.astimezone(timezone.utc)

6. Recurring End Date Handling

Check End Date

def should_create_next_occurrence(
    next_occurrence: datetime,
    recurring_end_date: datetime | None
) -> bool:
    """Check if next occurrence should be created."""
    if not recurring_end_date:
        return True  # No end date, infinite recurrence
    
    return next_occurrence <= recurring_end_date.replace(tzinfo=timezone.utc)

7. Edge Cases

DST Transitions

# RRULE handles DST automatically
# Use UTC to avoid DST issues
rule = rrule(
    DAILY,
    dtstart=datetime(2025, 3, 9, 10, 0, 0, tzinfo=timezone.utc),  # DST change day
    count=5
)

Leap Years

# RRULE handles leap years automatically
rule = rrule(
    YEARLY,
    dtstart=datetime(2024, 2, 29, 10, 0, 0, tzinfo=timezone.utc),  # Leap year
    count=5
)

8. Integration with Task Service

Update Next Occurrence

from sqlmodel import Session, select

async def update_next_occurrence(
    session: Session,
    task_id: int,
    parser: RRuleParser
) -> datetime | None:
    """Update next_occurrence field for recurring task."""
    task = session.get(Task, task_id)
    
    if not task or not task.recurring_pattern:
        return None
    
    # Calculate next occurrence
    next_occurrence = parser.calculate_next(
        pattern=task.recurring_pattern,
        dtstart=datetime.utcnow().replace(tzinfo=timezone.utc),
        end_date=task.recurring_end_date
    )
    
    if next_occurrence:
        task.next_occurrence = next_occurrence
        session.add(task)
        session.commit()
    
    return next_occurrence

Best Practices

1. Always Use UTC

# ✅ Good
dt = datetime.utcnow().replace(tzinfo=timezone.utc)

# ❌ Bad
dt = datetime.now()  # Local timezone

2. Validate Patterns

parser = RRuleParser()
if not parser.validate_pattern(pattern):
    raise ValueError(f"Invalid pattern: {pattern}")

3. Handle End Dates

if recurring_end_date and next_occurrence > recurring_end_date:
    return None  # Don't create next occurrence

4. Cache Parsed Rules

from functools import lru_cache

@lru_cache(maxsize=100)
def get_cached_rule(pattern: str):
    """Cache parsed RRULE patterns."""
    parser = RRuleParser()
    return parser.parse_pattern(pattern)

References