Skip to content

Commit 6ab9684

Browse files
committed
Fix duration mapping for all-day events
- Align DURATION with DTSTART type - Add tests for alignWithDtStart function
1 parent 47da9c1 commit 6ab9684

File tree

3 files changed

+79
-6
lines changed

3 files changed

+79
-6
lines changed

lib/src/main/kotlin/at/bitfire/ical4android/util/TimeApiExtensions.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ object TimeApiExtensions {
154154
if (secs == 0L)
155155
return "PT0S"
156156

157-
var weeks = secs / SECONDS_PER_WEEK
157+
val weeks = secs / SECONDS_PER_WEEK
158158
secs -= weeks * SECONDS_PER_WEEK
159159

160160
var days = secs / SECONDS_PER_DAY
@@ -170,7 +170,6 @@ object TimeApiExtensions {
170170
return "P${weeks}W"
171171

172172
days += weeks * DAYS_PER_WEEK
173-
weeks = 0
174173

175174
if (days != 0L)
176175
builder.append("${days}D")

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilder.kt

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import android.content.Entity
1010
import android.provider.CalendarContract.Events
1111
import androidx.annotation.VisibleForTesting
1212
import at.bitfire.ical4android.util.DateUtils
13+
import at.bitfire.ical4android.util.TimeApiExtensions.toDuration
1314
import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate
1415
import at.bitfire.ical4android.util.TimeApiExtensions.toRfc5545Duration
1516
import at.bitfire.synctools.icalendar.requireDtStart
@@ -21,6 +22,7 @@ import net.fortuna.ical4j.model.property.Duration
2122
import net.fortuna.ical4j.model.property.RDate
2223
import net.fortuna.ical4j.model.property.RRule
2324
import java.time.Period
25+
import java.time.temporal.TemporalAmount
2426

2527
class DurationBuilder: AndroidEntityBuilder {
2628

@@ -52,15 +54,52 @@ class DurationBuilder: AndroidEntityBuilder {
5254
so we wouldn't have to take care of that. However it expects seconds to be in "P<n>S" format,
5355
whereas we provide an RFC 5545-compliant "PT<n>S", which causes the provider to crash:
5456
https://github.com/bitfireAT/synctools/issues/144. So we must convert it ourselves to be on the safe side. */
57+
val alignedDuration = alignWithDtStart(duration.duration, dtStart)
5558

56-
// TODO: adapt DURATION according to DATE/DATE-TIME
59+
/* TemporalAmount can have months and years, but the RFC 5545 value must only contain weeks, days and time.
60+
So we have to recalculate the months/years to days according to their position in the calendar.
5761
58-
// TemporalAmount can have months and years, but the RFC 5545 must only contain weeks, days and time.
59-
// So we have to recalculate the months/years to days according to their position in the calendar.
60-
val durationStr = duration.duration.toRfc5545Duration(dtStart.date.toInstant())
62+
The calendar provider accepts every DURATION that `com.android.calendarcommon2.Duration` can parse,
63+
which is weeks, days, hours, minutes and seconds. */
64+
val durationStr = alignedDuration.toRfc5545Duration(dtStart.date.toInstant())
6165
values.put(Events.DURATION, durationStr)
6266
}
6367

68+
/**
69+
* Aligns the given temporal amount (taken from DURATION) to the VALUE-type (DATE-TIME/DATE) of DTSTART.
70+
*
71+
* @param amount temporal amount that shall be aligned
72+
* @param dtStart DTSTART to compare with
73+
*
74+
* @return Temporal amount that is
75+
*
76+
* - a [Period] (days/months/years that can't be represented by an exact number of seconds) when [dtStart] is a DATE, and
77+
* - a [java.time.Duration] (exact time that can be represented by an exact number of seconds) when [dtStart] is a DATE-TIME.
78+
*/
79+
@VisibleForTesting
80+
internal fun alignWithDtStart(amount: TemporalAmount, dtStart: DtStart): TemporalAmount {
81+
if (DateUtils.isDate(dtStart)) {
82+
// DTSTART is DATE
83+
return if (amount is java.time.Duration) {
84+
// amount is Duration, change to Period of seconds instead
85+
Period.ofDays(amount.toDays().toInt())
86+
} else {
87+
// amount is already Period
88+
amount
89+
}
90+
91+
} else {
92+
// DTSTART is DATE-TIME
93+
return if (amount is java.time.Period) {
94+
// amount is Period, change to Duration instead
95+
amount.toDuration(dtStart.date.toInstant())
96+
} else {
97+
// amount is already Duration
98+
amount
99+
}
100+
}
101+
}
102+
64103
@VisibleForTesting
65104
internal fun calculateFromDtEnd(dtStart: DtStart, dtEnd: DtEnd?): Duration? {
66105
if (dtEnd == null)

lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilderTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,41 @@ class DurationBuilderTest {
142142
}
143143

144144

145+
// alignWithDtStart
146+
147+
@Test
148+
fun `alignWithDtStart (DTSTART all-day, DURATION all-day)`() {
149+
assertEquals(
150+
Period.ofDays(1), // may not be 24 hours (for instance on DST switch)
151+
builder.alignWithDtStart(Period.ofDays(1), DtStart(Date()))
152+
)
153+
}
154+
155+
@Test
156+
fun `alignWithDtStart (DTSTART non-all-day, DURATION all-day)`() {
157+
assertEquals(
158+
java.time.Duration.ofDays(1), // exactly 24 hours
159+
builder.alignWithDtStart(Period.ofDays(1), DtStart(DateTime()))
160+
)
161+
}
162+
163+
@Test
164+
fun `alignWithDtStart (DTSTART all-day, DURATION non-all-day)`() {
165+
assertEquals(
166+
Period.ofDays(1), // may not be 24 hours (for instance on DST switch)
167+
builder.alignWithDtStart(java.time.Duration.ofDays(1), DtStart(Date()))
168+
)
169+
}
170+
171+
@Test
172+
fun `alignWithDtStart (DTSTART non-all-day, DURATION non-all-day)`() {
173+
assertEquals(
174+
java.time.Duration.ofDays(1), // exactly 24 hours
175+
builder.alignWithDtStart(Period.ofDays(1), DtStart(DateTime()))
176+
)
177+
}
178+
179+
145180
// calculateFromDtEnd
146181

147182
@Test

0 commit comments

Comments
 (0)