summaryrefslogtreecommitdiff
path: root/android/app/src/main/java/org/terst/doot/widget/ui/MultiDayEvents.kt
blob: a8f4659937f3b3b4f965747afcb536821f228117 (plain)
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)"
}