mangstadt / biweekly

biweekly is an iCalendar library written in Java.
BSD 2-Clause "Simplified" License
320 stars 44 forks source link

TimeZones Break between Months #130

Open bstock21 opened 6 months ago

bstock21 commented 6 months ago

I've encountered a situation where advancing hourly with DateIterator does not respect the rules for iterating.

Below I have sudo code which consists of an hourly Recurrence Rule and a date of Jan 31 at 18:00 CST. Which should iterate to Jan 31 at 19:00 CST but instead iterates to Feb 1 at 00:00 CST. Probably has something to do with the difference in time between UTC and current time zone CT. Please see my sudo code and advise.

// Happening with version 0.6.8

// Params

long after = 1706745600001; // 2024-01-31T18:00:00.001-0600
TimeZone timezone = TimeZone.getTimeZone("America/Chicago");
VEvent event = new VEvent();
event.setDateStart("Mon Jan 01 00:00:00 CST 2024")
event.setRecurrenceRule("[""rrule"", {}, ""recur"", {""freq"": ""HOURLY""}]]") // everything else is 0 or null

biweekly.component.VEvent
  biweekly.property.Uid [ parameters={} | value=c20b6b14-5ebb-47be-a167-eaa4cbc338b3 ]
  biweekly.property.DateTimeStamp [ parameters={} | value=Wed Mar 06 08:39:01 CST 2024 ]
  biweekly.property.DateStart [ parameters={} | value=Mon Jan 01 00:00:00 CST 2024 ]
  biweekly.property.RecurrenceRule [ parameters={} | value=biweekly.util.Recurrence@697f8f09 ]

// Code

DateIterator it = event.getDateIterator(timezone);
it.advanceTo(new Date(after)); // after = 2024-01-31T18:00:00.001-0600
while (it.hasNext()) {
    Date next = it.next(); // next == Thu Feb 01 00:00:00 CST 2024
}   
mangstadt commented 6 months ago

biweekly.util.com.google.ical.compat.javautil.DateIteratorFactory, line 100: This is the advanceTo implementation that is called.

biweekly.util.com.google.ical.compat.javautil.DateIteratorFactory, line 159: In this method, the Date object is converted to a DateValue object, which represents the Date object in UTC time. Then, it checks to see if the DateValue object's time component is midnight. In this case, the time component is midnight because 18:00 CST is 00:00 UTC. If it is midnight, then it treats it as a date value instead of a datetime value. This is what causes the problem. If this if statement is removed, the DateIterator works as expected.

bstock21 commented 6 months ago

That change is causing some issues with the createDateIterableUntimed test. When itterator.next() is called it's ignoring the Time Zone and just going to the next date.

image

bstock21 commented 1 month ago

For anyone who finds this. Originally, I tried to work around this by just offsetting the time by 1 minute so it would never be midnight UTC. I encountered further issues though. Ultimately I just wrote a small iterator that suited my purposes.

`import biweekly.util.Frequency;

import java.util.Calendar;
import java.util.Date;

// This custom DateIterator exists because the date iterator from BiWeekly library is broken.
public class CustomDateIterator {
    private final Calendar calendar;
    private final Frequency intervalType;

    public CustomDateIterator(Date startDate, Frequency intervalType) {
        this.calendar = Calendar.getInstance();
        this.calendar.setTime(startDate);
        this.intervalType = intervalType;
    }

    public Date next() {
        addInterval();
        Date currentDate = calendar.getTime();
        return currentDate;
    }
    private void addInterval() {
        switch (intervalType) {
        case HOURLY:
            calendar.add(Calendar.HOUR, 1);
            break;
        case DAILY:
            calendar.add(Calendar.DAY_OF_YEAR, 1);
            break;
        case WEEKLY:
            calendar.add(Calendar.WEEK_OF_YEAR, 1);
            break;
        case MONTHLY:
            calendar.add(Calendar.MONTH, 1);
            break;
        }
    }

    public void reset(Date startDate) {
        calendar.setTime(startDate);
    }
}`