@@ -9,13 +9,21 @@ package at.bitfire.synctools.mapping.calendar.processor
99import android.content.Entity
1010import android.provider.CalendarContract.Events
1111import at.bitfire.ical4android.Event
12+ import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate
13+ import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDateTime
14+ import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate
15+ import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime
1216import at.bitfire.synctools.exception.InvalidLocalResourceException
1317import at.bitfire.synctools.util.AndroidTimeUtils
18+ import net.fortuna.ical4j.model.Date
19+ import net.fortuna.ical4j.model.DateTime
20+ import net.fortuna.ical4j.model.Recur
1421import net.fortuna.ical4j.model.TimeZoneRegistry
1522import net.fortuna.ical4j.model.property.ExDate
1623import net.fortuna.ical4j.model.property.ExRule
1724import net.fortuna.ical4j.model.property.RDate
1825import net.fortuna.ical4j.model.property.RRule
26+ import java.time.ZonedDateTime
1927import java.util.LinkedList
2028import java.util.logging.Level
2129import java.util.logging.Logger
@@ -33,13 +41,26 @@ class RecurrenceFieldsProcessor(
3341 val tsStart = values.getAsLong(Events .DTSTART ) ? : throw InvalidLocalResourceException (" Found event without DTSTART" )
3442 val allDay = (values.getAsInteger(Events .ALL_DAY ) ? : 0 ) != 0
3543
44+ // provide start date as ical4j Date, if needed
45+ val startDate by lazy {
46+ AndroidTimeField (
47+ timestamp = tsStart,
48+ timeZone = values.getAsString(Events .EVENT_TIMEZONE ),
49+ allDay = allDay,
50+ tzRegistry = tzRegistry
51+ ).asIcal4jDate()
52+ }
53+
3654 // process RRULE field
3755 val rRules = LinkedList <RRule >()
3856 values.getAsString(Events .RRULE )?.let { rRuleField ->
3957 try {
4058 for (rule in rRuleField.split(AndroidTimeUtils .RECURRENCE_RULE_SEPARATOR )) {
4159 val rule = RRule (rule)
4260
61+ // align RRULE UNTIL to DTSTART, if needed
62+ rule.recur = alignUntil(rule.recur, startDate)
63+
4364 // skip if UNTIL is before event's DTSTART
4465 val tsUntil = rule.recur.until?.time
4566 if (tsUntil != null && tsUntil <= tsStart) {
@@ -74,6 +95,9 @@ class RecurrenceFieldsProcessor(
7495 for (rule in exRuleField.split(AndroidTimeUtils .RECURRENCE_RULE_SEPARATOR )) {
7596 val rule = ExRule (null , rule)
7697
98+ // align RRULE UNTIL to DTSTART, if needed
99+ rule.recur = alignUntil(rule.recur, startDate)
100+
77101 // skip if UNTIL is before event's DTSTART
78102 val tsUntil = rule.recur.until?.time
79103 if (tsUntil != null && tsUntil <= tsStart) {
@@ -109,4 +133,53 @@ class RecurrenceFieldsProcessor(
109133 }
110134 }
111135
136+ /* *
137+ * Aligns the `UNTIL` of the given recurrence info to the VALUE-type (DATE-TIME/DATE) of [startDate].
138+ *
139+ * @param recur recurrence info whose `UNTIL` shall be aligned
140+ * @param startDate `DTSTART` date to compare with
141+ *
142+ * @return
143+ *
144+ * - UNTIL not set → original recur
145+ * - UNTIL and DTSTART are both either DATE or DATE-TIME → original recur
146+ * - UNTIL is DATE, DTSTART is DATE-TIME → UNTIL is amended to DATE-TIME with time and timezone from DTSTART
147+ * - UNTIL is DATE-TIME, DTSTART is DATE → UNTIL is reduced to its date component
148+ *
149+ * @see at.bitfire.synctools.mapping.calendar.builder.EndTimeBuilder.alignWithDtStart
150+ */
151+ fun alignUntil (recur : Recur , startDate : Date ): Recur {
152+ val until: Date ? = recur.until
153+ if (until == null )
154+ return recur
155+
156+ if (until is DateTime ) {
157+ // UNTIL is DATE-TIME
158+ if (startDate is DateTime ) {
159+ // DTSTART is DATE-TIME
160+ return recur
161+ } else {
162+ // DTSTART is DATE → only take date part
163+ val untilDate = until.toLocalDate()
164+ return Recur .Builder (recur)
165+ .until(untilDate.toIcal4jDate())
166+ .build()
167+ }
168+ } else {
169+ // UNTIL is DATE
170+ if (startDate is DateTime ) {
171+ // DTSTART is DATE-TIME
172+ val untilDate = until.toLocalDate()
173+ val startTime = startDate.toZonedDateTime()
174+ val untilDateWithTime = ZonedDateTime .of(untilDate, startTime.toLocalTime(), startTime.zone)
175+ return Recur .Builder (recur)
176+ .until(untilDateWithTime.toIcal4jDateTime())
177+ .build()
178+ } else {
179+ // DTSTART is DATE
180+ return recur
181+ }
182+ }
183+ }
184+
112185}
0 commit comments