* The {@link EventFilterHandler} filters events from a calendar and presents them in a dynamic way.
*
* @author Michael Wodniok - Initial Contribution
+ * @author Michael Wodniok - Fixed subsecond search if rounding to unit
*/
@NonNullByDefault
public class EventFilterHandler extends BaseThingHandler implements CalendarUpdateListener {
case HOUR:
refDT = refDT.with(ChronoField.MINUTE_OF_HOUR, 0);
case MINUTE:
- refDT = refDT.with(ChronoField.SECOND_OF_MINUTE, 0);
+ refDT = refDT.with(ChronoField.SECOND_OF_MINUTE, 0).with(ChronoField.NANO_OF_SECOND, 0);
}
reference = refDT.toInstant();
}
import biweekly.io.TimezoneAssignment;
import biweekly.io.TimezoneInfo;
import biweekly.io.text.ICalReader;
+import biweekly.parameter.Range;
import biweekly.property.Comment;
import biweekly.property.Contact;
import biweekly.property.DateEnd;
import biweekly.property.Description;
import biweekly.property.DurationProperty;
import biweekly.property.Location;
+import biweekly.property.RecurrenceId;
import biweekly.property.Status;
import biweekly.property.Summary;
import biweekly.property.TextProperty;
import biweekly.property.Uid;
+import biweekly.util.ICalDate;
import biweekly.util.com.google.ical.compat.javautil.DateIterator;
/**
* @author Michael Wodniok - Initial contribution
* @author Andrew Fiddian-Green - Methods getJustBegunEvents() & getJustEndedEvents()
* @author Michael Wodniok - Extension for filtered events
+ * @author Michael Wodniok - Added logic for events moved with "RECURRENCE-ID" (issue 9647)
*/
@NonNullByDefault
class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
int foundInSeries = 0;
while (positiveBeginDates.hasNext()) {
final Instant begInst = positiveBeginDates.next().toInstant();
- if (begInst.isAfter(frameEnd)) {
+ if (begInst.isAfter(frameEnd) || begInst.equals(frameEnd)) {
break;
}
Duration duration = getEventLength(positiveEvent);
for (final VEvent currentEvent : usedCalendar.getEvents()) {
final Status eventStatus = currentEvent.getStatus();
boolean positive = (eventStatus == null || (eventStatus.isTentative() || eventStatus.isConfirmed()));
- final Collection<VEvent> positiveOrNegativeEvents = (positive ? positiveEvents : negativeEvents);
- positiveOrNegativeEvents.add(currentEvent);
+ final RecurrenceId eventRecurrenceId = currentEvent.getRecurrenceId();
+ if (positive && eventRecurrenceId != null) {
+ // RecurrenceId moves an event. This blocks other events of series and creates a new single instance
+ positiveEvents.add(currentEvent);
+ negativeEvents.add(currentEvent);
+ } else {
+ final Collection<VEvent> positiveOrNegativeEvents = (positive ? positiveEvents : negativeEvents);
+ positiveOrNegativeEvents.add(currentEvent);
+ }
}
}
for (final VEvent counterEvent : counterEvents) {
final Uid counterEventUid = counterEvent.getUid();
if (counterEventUid != null && eventUid.getValue().contentEquals(counterEventUid.getValue())) {
- final DateIterator counterStartDates = getRecurredEventDateIterator(counterEvent);
- counterStartDates.advanceTo(Date.from(startInstant));
- if (counterStartDates.hasNext()) {
- final Instant counterStartInstant = counterStartDates.next().toInstant();
- if (counterStartInstant.equals(startInstant)) {
- return true;
+ final RecurrenceId counterRecurrenceId = counterEvent.getRecurrenceId();
+ if (counterRecurrenceId != null) {
+ ICalDate recurrenceDate = counterRecurrenceId.getValue();
+ if (recurrenceDate != null) {
+ Instant recurrenceInstant = Instant.ofEpochMilli(recurrenceDate.getTime());
+ if (recurrenceInstant.equals(startInstant)) {
+ return true;
+ }
+ Range futureOrPast = counterRecurrenceId.getRange();
+ if (futureOrPast != null && futureOrPast.equals(Range.THIS_AND_FUTURE)
+ && startInstant.isAfter(recurrenceInstant)) {
+ return true;
+ }
+ if (futureOrPast != null && futureOrPast.equals(Range.THIS_AND_PRIOR)
+ && startInstant.isBefore(recurrenceInstant)) {
+ return true;
+ }
+ }
+ } else {
+ final DateIterator counterStartDates = getRecurredEventDateIterator(counterEvent);
+ counterStartDates.advanceTo(Date.from(startInstant));
+ if (counterStartDates.hasNext()) {
+ final Instant counterStartInstant = counterStartDates.next().toInstant();
+ if (counterStartInstant.equals(startInstant)) {
+ return true;
+ }
}
}
}
import java.io.FileInputStream;
import java.io.IOException;
import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
private AbstractPresentableCalendar calendar;
private AbstractPresentableCalendar calendar2;
private AbstractPresentableCalendar calendar3;
+ private AbstractPresentableCalendar calendar_issue9647;
@BeforeEach
public void setUp() throws IOException, CalendarException {
calendar = new BiweeklyPresentableCalendar(new FileInputStream("src/test/resources/test.ics"));
calendar2 = new BiweeklyPresentableCalendar(new FileInputStream("src/test/resources/test2.ics"));
calendar3 = new BiweeklyPresentableCalendar(new FileInputStream("src/test/resources/test3.ics"));
+ calendar_issue9647 = new BiweeklyPresentableCalendar(
+ new FileInputStream("src/test/resources/test-issue9647.ics"));
}
/**
List<Event> realFilteredEvents6 = calendar.getFilteredEventsBetween(Instant.parse("2019-09-15T06:00:00Z"),
Instant.parse("2019-12-31T00:00:00Z"), null, 3);
assertEquals(0, realFilteredEvents6.size());
+
+ List<Event> realFilteredEvents7 = calendar_issue9647.getFilteredEventsBetween(
+ LocalDate.parse("2021-01-01").atStartOfDay(ZoneId.systemDefault()).toInstant(),
+ LocalDate.parse("2021-01-02").atStartOfDay(ZoneId.systemDefault()).toInstant(), null, 3);
+ assertEquals(0, realFilteredEvents7.size());
+
+ Event[] expectedFilteredEvents8 = new Event[] {
+ new Event("Restabfall", LocalDate.parse("2021-01-04").atStartOfDay(ZoneId.systemDefault()).toInstant(),
+ LocalDate.parse("2021-01-05").atStartOfDay(ZoneId.systemDefault()).toInstant(), ""),
+ new Event("Gelbe Tonne", LocalDate.parse("2021-01-04").atStartOfDay(ZoneId.systemDefault()).toInstant(),
+ LocalDate.parse("2021-01-05").atStartOfDay(ZoneId.systemDefault()).toInstant(), "") };
+ List<Event> realFilteredEvents8 = calendar_issue9647.getFilteredEventsBetween(
+ LocalDate.parse("2021-01-04").atStartOfDay(ZoneId.systemDefault()).toInstant(),
+ LocalDate.parse("2021-01-05").atStartOfDay(ZoneId.systemDefault()).toInstant(), null, 3);
+ assertArrayEquals(expectedFilteredEvents8, realFilteredEvents8.toArray(new Event[] {}));
}
}
--- /dev/null
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:PUBLISH
+X-WR-CALNAME:Entsorgung
+X-WR-TIMEZONE:Europe/Amsterdam
+X-WR-CALDESC:Entleerungstermine Mülltonnen
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20210104
+DTEND;VALUE=DATE:20210105
+DTSTAMP:20210102T150339Z
+UID:pseudo7346893o7r8328zheh@google.com
+RECURRENCE-ID;VALUE=DATE:20210101
+CREATED:20200708T110847Z
+DESCRIPTION:
+LAST-MODIFIED:20210102T135837Z
+LOCATION:
+SEQUENCE:8
+STATUS:CONFIRMED
+SUMMARY:Restabfall
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20210104
+DTEND;VALUE=DATE:20210105
+DTSTAMP:20210102T150339Z
+UID:pseudo352763jug7bhd7528237t@google.com
+RECURRENCE-ID;VALUE=DATE:20210101
+CREATED:20200708T110659Z
+DESCRIPTION:
+LAST-MODIFIED:20210102T135834Z
+LOCATION:
+SEQUENCE:8
+STATUS:CONFIRMED
+SUMMARY:Gelbe Tonne
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20201226
+DTEND;VALUE=DATE:20201227
+DTSTAMP:20210102T150339Z
+UID:pseudo32zhibh75fvdv763b713@google.com
+RECURRENCE-ID;VALUE=DATE:20201225
+CREATED:20200708T110333Z
+DESCRIPTION:
+LAST-MODIFIED:20200708T111046Z
+LOCATION:
+SEQUENCE:1
+STATUS:CONFIRMED
+SUMMARY:Blaue Tonne
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20200717
+DTEND;VALUE=DATE:20200718
+RRULE:FREQ=WEEKLY;WKST=MO;INTERVAL=2;BYDAY=FR
+DTSTAMP:20210102T150339Z
+UID:pseudo7346893o7r8328zheh@google.com
+CREATED:20200708T110847Z
+DESCRIPTION:
+LAST-MODIFIED:20200708T110851Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Restabfall
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20200714
+DTEND;VALUE=DATE:20200715
+RRULE:FREQ=WEEKLY;WKST=MO;BYDAY=TU
+DTSTAMP:20210102T150339Z
+UID:pseudolo2p0394z2edxc1223@google.com
+CREATED:20200708T110802Z
+DESCRIPTION:
+LAST-MODIFIED:20200708T110820Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Bio-Tonne
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20200717
+DTEND;VALUE=DATE:20200718
+RRULE:FREQ=WEEKLY;WKST=MO;INTERVAL=2;BYDAY=FR
+DTSTAMP:20210102T150339Z
+UID:pseudo352763jug7bhd7528237t@google.com
+CREATED:20200708T110659Z
+DESCRIPTION:
+LAST-MODIFIED:20200708T110729Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Gelbe Tonne
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20200710
+DTEND;VALUE=DATE:20200711
+RRULE:FREQ=WEEKLY;WKST=MO;INTERVAL=2;BYDAY=FR
+DTSTAMP:20210102T150339Z
+UID:pseudo32zhibh75fvdv763b713@google.com
+CREATED:20200708T110333Z
+DESCRIPTION:
+LAST-MODIFIED:20200708T110720Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Blaue Tonne
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR