ical4j / ical4j

A Java library for parsing and building iCalendar data models
https://www.ical4j.org
BSD 3-Clause "New" or "Revised" License
749 stars 200 forks source link

calculateRecurrenceSet produces invalid data after DST shift #661

Open RoarN opened 10 months ago

RoarN commented 10 months ago

Using version: 4.0.0-rc1

It looks like there is a bug regarding DST in the calculateRecurrenceSet() method. For the time zone Europe/Oslo there should be a change in the offset on sunday 2024-10-27, the rules in the Oslo.ics file looks correct so it must be how the data is used that is the issue.

It is also peculiar that the time zone in the calendar (Europe/Oslo) is not used in the result generated by calculateRecurrenceSet() instead of a placeholder time timezone (ical4j~5edc949e-52b8-4522-84d9-4415872637c2). This also makes the dates (ZonedDateTime) returned very difficult to use as is.

The Code

import java.io.IOException; import java.io.StringReader; import java.text.ParseException; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.Temporal; import java.util.Set; import net.fortuna.ical4j.data.CalendarBuilder; import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.model.Period;

public class MainDST { public static void main(String[] args) throws ParseException, IOException, ParserException { System.out.println("Starting"); testDST(); }

public static void testDST() throws ParseException, IOException, ParserException
{
    String calendarString = "BEGIN:VCALENDAR\r\n"
            + "PRODID:-//CeevView//abc Calendar 1.0//EN\r\n"
            + "VERSION:2.0\r\n"
            + "CALSCALE:GREGORIAN\r\n"
            + "BEGIN:VEVENT\r\n"
            + "DTSTAMP:20231030T095718Z\r\n"
            + "SUMMARY:Full Day\r\n"
            + "UID:74ed1fcb-f8f4-4f35-97a4-f6afacf25e6e@abc.com\r\n"
            + "CREATED:20231030T095718Z\r\n"
            + "RRULE:FREQ=DAILY\r\n"
            + "DTSTART;TZID=Europe/Oslo:20220101T000000\r\n"
            + "DTEND;TZID=Europe/Oslo:20220102T000000\r\n"
            + "END:VEVENT\r\n"
            + "END:VCALENDAR";

    CalendarBuilder builder = new CalendarBuilder();
    StringReader sin = new StringReader(calendarString);

    Calendar calendar = builder.build(sin);
    System.out.println(calendar.toString());

    System.out.println("---");
    System.out.println("Printing the events generated using calculateRecurrenceSet()");
    for (Object o : calendar.getComponents("VEVENT"))
    {
        Component c = (Component) o;            

        Period<Temporal> period = new Period<Temporal>(LocalDateTime.parse("2024-10-25T00:00:00"), LocalDateTime.parse("2024-10-29T00:00:00"));
        Set<Period<Temporal>> list = c.calculateRecurrenceSet(period);

        for (Object object : list)
        {
            Period<?> p = (Period<?>) object;
            System.out.println(p.getStart() + " -> " + p.getEnd() + ": " + p.getDuration());                
        }
    }
    System.out.println("---");
    System.out.println("Printing the same dates as above using the time zone from the calendar with ZonedDateTime");
    ZonedDateTime t;
    ZoneId tz =  ZoneId.of("Europe/Oslo");

    t =  LocalDateTime.parse("2024-10-25T00:00:00").atZone(tz); System.out.println(t);
    t =  LocalDateTime.parse("2024-10-26T00:00:00").atZone(tz); System.out.println(t);
    t =  LocalDateTime.parse("2024-10-27T00:00:00").atZone(tz); System.out.println(t);
    t =  LocalDateTime.parse("2024-10-28T00:00:00").atZone(tz); System.out.println(t);
    t =  LocalDateTime.parse("2024-10-29T00:00:00").atZone(tz); System.out.println(t);
}

}

The Result

BEGIN:VCALENDAR PRODID:-//CeevView//abc Calendar 1.0//EN VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20231030T095718Z SUMMARY:Full Day UID:74ed1fcb-f8f4-4f35-97a4-f6afacf25e6e@abc.com CREATED:20231030T095718Z RRULE:FREQ=DAILY DTSTART;TZID=Europe/Oslo:20220101T000000 DTEND;TZID=Europe/Oslo:20220102T000000 END:VEVENT END:VCALENDAR


Printing the events generated using calculateRecurrenceSet() 2024-10-24T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2] -> 2024-10-25T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2]: PT24H 2024-10-25T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2] -> 2024-10-26T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2]: PT24H 2024-10-26T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2] -> 2024-10-27T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2]: PT24H 2024-10-27T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2] -> 2024-10-28T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2]: PT24H 2024-10-28T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2] -> 2024-10-29T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2]: PT24H 2024-10-29T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2] -> 2024-10-30T00:00+02:00[ical4j~5edc949e-52b8-4522-84d9-4415872637c2]: PT24H

Printing the same dates as above using the time zone from the calendar with ZonedDateTime 2024-10-25T00:00+02:00[Europe/Oslo] 2024-10-26T00:00+02:00[Europe/Oslo] 2024-10-27T00:00+02:00[Europe/Oslo] 2024-10-28T00:00+01:00[Europe/Oslo] 2024-10-29T00:00+01:00[Europe/Oslo]

hagmo commented 10 months ago

I'm having what I think is a similar problem. There are at least 2 issues here, I think. One has been around for a long time, the other may have been introduced in ical4j 3.0.2.

Here are some test cases to illustrate.

/**
 * This works in 3.0.1, not in 3.0.2.
 */
public void minimalExampleNew() throws ParseException {
    System.out.println("Using " + TimeZone.class.getProtectionDomain().getCodeSource().getLocation().getFile());
    TimeZone timeZone = TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone("Europe/Berlin");

    //Create recurring VEVENT
    DateTime startTime = new DateTime("20191019T19000", timeZone);
    DateTime endTime = new DateTime("20191021T070000", timeZone);
    VEvent vEvent = new VEvent(startTime, endTime, "Summary");
    PropertyList properties = vEvent.getProperties();
    properties.add(new RRule("FREQ=WEEKLY"));

    //Produce specific occurrence of event.
    //Starting on Saturday before summer time ends,
    //ending on Monday after summer time ended.
    DateTime recurrenceStart = new DateTime("20191026T120000", timeZone);
    DateTime recurrenceEnd = new DateTime("20191028T000000", timeZone);
    Period period = new Period(recurrenceStart, recurrenceEnd);
    PeriodList periodList = vEvent.calculateRecurrenceSet(period);
    Assert.assertEquals(periodList.size(), 1);
    Period p = periodList.stream().findFirst().orElseThrow(RuntimeException::new);
    VEvent e = new VEvent(p.getStart(), p.getEnd(), "Occurrence");
    Assert.assertTrue(e.toString().contains("DTSTART;TZID=Europe/Berlin:20191026T190000"), "Start time: " + e);
    //Below fails in 3.0.2 and up: the actual line is DTEND;TZID=Europe/Berlin:20191028T060000.
    Assert.assertTrue(e.toString().contains("DTEND;TZID=Europe/Berlin:20191028T070000"), "End time: " + e);
}

/**
 * This fails as far back as 1.0.6
 */
public void minimalExampleOld() throws ParseException {
    System.out.println("Using " + TimeZone.class.getProtectionDomain().getCodeSource().getLocation().getFile());
    TimeZone timeZone = TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone("Europe/Berlin");

    //Create recurring VEVENT
    DateTime startTime = new DateTime("20191019T19000", timeZone);
    DateTime endTime = new DateTime("20191020T070000", timeZone);
    VEvent vEvent = new VEvent(startTime, endTime, "Summary");
    PropertyList properties = vEvent.getProperties();
    properties.add(new RRule("FREQ=WEEKLY"));

    //Produce specific occurrence of event.
    //Starting on Saturday before summer time ends,
    //ending on Sunday, the day when summer time ends.
    DateTime recurrenceStart = new DateTime("20191026T120000", timeZone);
    DateTime recurrenceEnd = new DateTime("20191028T000000", timeZone);
    Period period = new Period(recurrenceStart, recurrenceEnd);
    PeriodList periodList = vEvent.calculateRecurrenceSet(period);
    Period p = periodList.stream().findFirst().orElseThrow(RuntimeException::new);
    VEvent e = new VEvent(p.getStart(), p.getEnd(), "Occurrence");
    Assert.assertTrue(e.toString().contains("DTSTART;TZID=Europe/Berlin:20191026T190000"), "Start time: " + e);
    //Below fails: The actual line is DTEND;TZID=Europe/Berlin:20191027T060000
    Assert.assertTrue(e.toString().contains("DTEND;TZID=Europe/Berlin:20191027T070000"), "End time: " + e);
}

If the end time of a recurring event falls on the same day as the zone offset switch, it's always wrong, according to minimalExampleOld.

The other case, where the occurrence starts before the switch and ends after, but does not end on the actual day of the switch, only fails in version 3.0.2 and up.

hagmo commented 10 months ago

I apologize for polluting this issue report. It turns out that both my test cases succeed in 4.0.0-rc1.

benfortuna commented 8 months ago

@RoarN there was a bug related to DST transitions in 4.0.0-rc1. A fix has just been implemented in 4.0.0-rc2, so this could be resolved also.

When I get a chance I will try to test this to confirm.

rjagarla commented 7 months ago

I am facing the Same issue for PST time Zone from Ical4j V1 to 4.0.0 - rc3. We had an event which was supposed to start 02-Nov-2024 16:00 PDT to 03-Nov-2024 09:00 PST but the event closed an hour earlier i.e 03-Nov-2024 08:00 PST. We observed that calculateRecurrenceSet produces invalid data after DST shift.

benfortuna commented 7 months ago

Pls provide details of timezone and if possible a sample file that can be used to test.

rjagarla commented 6 months ago

We are seeing this on America/Los_Angeles time zone below is our ical file

`BEGIN:VCALENDAR METHOD:PUBLISH VERSION:2.0 X-WR-CALNAME: Test PRODID:-//Apple Inc.//macOS 11.6.1//EN X-APPLE-CALENDAR-COLOR:#0E61B9 X-WR-TIMEZONE:America/Vancouver CALSCALE:GREGORIAN BEGIN:VTIMEZONE TZID:America/Los_Angeles BEGIN:DAYLIGHT TZOFFSETFROM:-0800 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU DTSTART:20070311T020000 TZNAME:PDT TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:-0700 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU DTSTART:20071104T020000 TZNAME:PST TZOFFSETTO:-0800 END:STANDARD END:VTIMEZONE BEGIN:VEVENT TRANSP:OPAQUE DTEND;TZID=America/Los_Angeles:20180609T090000 X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC UID:80CF5D17-9E86-4CA5-AA7A-6DE57FAC06CA EXDATE;TZID=America/Los_Angeles:20211210T160000 EXDATE;TZID=America/Los_Angeles:20211209T160000 EXDATE;TZID=America/Los_Angeles:20211213T160000 EXDATE;TZID=America/Los_Angeles:20211211T160000 EXDATE;TZID=America/Los_Angeles:20211212T160000 DTSTAMP:20180607T230736Z SEQUENCE:0 SUMMARY:Daily Blocker LAST-MODIFIED:20211210T215951Z DTSTART;TZID=America/Los_Angeles:20180608T160000 CREATED:20160610T200037Z RRULE:FREQ=DAILY END:VEVENT END:VCALENDAR

`

RoarN commented 6 months ago

I just checked the progress on this issue in 4.0.0-rc5. Some of the issues like changing the UTC offset after DST and using the actual time zone looks much better now.

However, one of the main problems is still there. The event on the day with DST ends at 23:00 NOT 00:00 as specified in the calendar. image

This is a problem for any event that starts before DST and ends after.