Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unwrap multi-day events feature #191

Open
RPdenBoer opened this issue Oct 9, 2024 · 2 comments
Open

Unwrap multi-day events feature #191

RPdenBoer opened this issue Oct 9, 2024 · 2 comments

Comments

@RPdenBoer
Copy link

RPdenBoer commented Oct 9, 2024

It would be useful to detect multi-day events (events where DTEND spans more than one day, or ticks over midnight, compared to DTSTART) and unwrap them into individual day-long events. I'm not aware of current support for this?

Example:
For an event starting on October 1st at 9:00 AM and ending on October 3rd at 5:00 PM, the feature would unwrap this into three events:

Event 1: October 1st, 9:00 AM – 11:59 PM
Event 2: October 2nd, (all-day event)
Event 3: October 3rd, 12:00 AM – 5:00 PM


We're using Polar.sh so you can upvote and help fund this issue. We receive the funding once the issue is completed & confirmed by you. Thank you in advance for helping prioritize & fund our work. Fund with Polar
@RPdenBoer
Copy link
Author

Here is a hacked-together example:

def unwrap_multi_day_events(calendar):
    new_calendar = Calendar()

    for component in calendar.walk("VEVENT"):
        event_start = component.get("DTSTART").dt
        event_end = component.get("DTEND").dt

        # Check if it's an all-day event (i.e., event_start is a date without time)
        is_all_day_event = isinstance(event_start, datetime) == False

        if is_all_day_event:
            # For all-day events, handle splitting only if it spans more than one day
            if (event_end - event_start).days > 1:
                # Multi-day all-day event: split into multiple single-day events
                current_day = event_start
                while current_day < event_end:
                    new_event = Event()
                    new_event.add("SUMMARY", component.get("SUMMARY"))
                    new_event.add("UID", component.get("UID") + f"-{current_day}")
                    new_event.add("DTSTART", current_day)
                    next_day = current_day + timedelta(days=1)
                    new_event.add("DTEND", next_day)
                    new_calendar.add_component(new_event)
                    current_day = next_day
            else:
                # Single day all-day event: preserve as-is
                new_calendar.add_component(component)
        else:
            # This is a timed event, possibly spanning multiple days
            start_datetime = event_start
            end_datetime = event_end

            # If the event spans multiple days, unwrap it
            if end_datetime.date() > start_datetime.date():
                current_day = start_datetime
                while current_day.date() < end_datetime.date():
                    new_event = Event()
                    new_event.add("SUMMARY", component.get("SUMMARY"))
                    new_event.add(
                        "UID", component.get("UID") + f"-{current_day.date()}"
                    )

                    if current_day == start_datetime:
                        # First day: keep the original start time and go till the end of the day
                        new_event.add("DTSTART", current_day)
                        next_day_start = (current_day + timedelta(days=1)).replace(
                            hour=0, minute=0, second=0
                        )
                        new_event.add("DTEND", next_day_start)
                    else:
                        # Intermediate days: treat as all-day events (from midnight to midnight)
                        new_event.add("DTSTART", current_day.date())
                        next_day = current_day + timedelta(days=1)
                        new_event.add("DTEND", next_day.date())

                    new_calendar.add_component(new_event)
                    current_day = current_day + timedelta(days=1)

                # Add the final day with the original end time
                final_event = Event()
                final_event.add("SUMMARY", component.get("SUMMARY"))
                final_event.add("UID", component.get("UID") + f"-{end_datetime.date()}")
                final_event.add(
                    "DTSTART", end_datetime.replace(hour=0, minute=0, second=0)
                )  # Start at midnight of the last day
                final_event.add("DTEND", end_datetime)  # End at the original end time
                new_calendar.add_component(final_event)
            else:
                # Single day timed event: preserve it as-is
                new_calendar.add_component(component)

    return new_calendar

@niccokunzmann
Copy link
Owner

Hi, thanks for reporting this. I have a few suggestions:

Event 1: October 1st, 9:00 AM – 11:59 PM
Event 2: October 2nd, (all-day event)

11:59 PM could also be midnight because the end is not inclusive.

I was wondering if one can create a function: chop(event, size=timedelta(days=1)) -> list[Event]
This would chop the event up into several events, aligned with the size.

Also, due to the way how recurrences are calculated, I think it would be best to chop up the events after recurring-ical-events has made the calculations. E.g. they sometimes depend on a weekday or they have a recurrence that ends at a specific date. In that case one might loose events from the results if the calendar is modified before it goes into of(...)

How would you like to proceed with this?

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

No branches or pull requests

2 participants