1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
package org.terst.doot.widget.ui
import org.terst.doot.widget.data.*
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
enum class MultiDayVariant { NONE, STARTS, ENDS, SPANS }
/**
* The last calendar day an item's occurrence actually covers. Google
* Calendar's all-day end date is EXCLUSIVE -- a single-day all-day event on
* July 13 has end=July14 00:00 (one day past its own day; mirrors the same
* convention already handled server-side, see effectiveEndDay in
* internal/handlers/timeline.go) -- so for an all-day item this subtracts
* one day to get the real last day. Timed events' end is already the real
* end instant, so no adjustment is needed there.
*/
private fun effectiveEndDay(end: Instant, zone: ZoneId, isAllDay: Boolean): LocalDate {
val day = end.atZone(zone).toLocalDate()
return if (isAllDay) day.minusDays(1) else day
}
/** True for a calendar event whose start and (adjusted) end fall on different calendar days. */
internal fun isMultiDayEvent(item: WidgetItem, zone: ZoneId): Boolean {
if (item.type != "event") return false
val start = item.start?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return false
val end = item.end?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return false
val startDay = start.atZone(zone).toLocalDate()
val endDay = effectiveEndDay(end, zone, item.isAllDay)
return endDay.isAfter(startDay)
}
/**
* How a multi-day item should render on renderDay. Caller must have already
* confirmed isMultiDayEvent(item) so item.start/item.end are non-null.
*/
internal fun multiDayVariant(item: WidgetItem, renderDay: LocalDate, zone: ZoneId): MultiDayVariant {
val start = Instant.parse(item.start!!).atZone(zone).toLocalDate()
val end = effectiveEndDay(Instant.parse(item.end!!), zone, item.isAllDay)
return when {
renderDay.isEqual(start) -> MultiDayVariant.STARTS
renderDay.isEqual(end) -> MultiDayVariant.ENDS
renderDay.isAfter(start) && renderDay.isBefore(end) -> MultiDayVariant.SPANS
else -> MultiDayVariant.NONE
}
}
/** "" for a plain all-day row or a "spans"-through day; " (starts/ends HH:MM)" otherwise. */
internal fun multiDayLabel(event: WidgetItem, variant: MultiDayVariant): String = when (variant) {
MultiDayVariant.STARTS -> event.start?.let { timeSuffix("starts", it) } ?: ""
MultiDayVariant.ENDS -> event.end?.let { timeSuffix("ends", it) } ?: ""
else -> ""
}
private fun timeSuffix(verb: String, iso: String): String {
val t = Instant.parse(iso).atZone(ZoneId.systemDefault())
if (t.hour == 0 && t.minute == 0) return ""
val minutePart = if (t.minute > 0) t.minute.toString().padStart(2, '0') else ""
return " ($verb ${hourLabel(t.hour)}$minutePart)"
}
|