Buschauffeurswekker v1: CalDAV-dagcheck 12:00, kwartier-afronding, wekkers Opstaan/Wally/Vertrek, fullscreen alarm-UI

This commit is contained in:
eddy 2026-10-14 16:31:50 +02:00
commit 34505be9b8
31 changed files with 1881 additions and 1 deletions

View file

@ -0,0 +1,138 @@
package nl.eddydevink.buschauffeurswekker.caldav
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class IcalTest {
private val amsterdam = ZoneId.of("Europe/Amsterdam")
@Test
fun `extractCalendarBlocks haalt blokken met elk namespace-prefix`() {
val xml = """
<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:response>
<d:href>/remote.php/dav/calendars/eddy/ebs_diensten-1/abc.ics</d:href>
<d:propstat><d:prop>
<c:calendar-data>BEGIN:VCALENDAR${'\n'}X:een</c:calendar-data>
</d:prop></d:propstat>
</d:response>
<d:response>
<d:propstat><d:prop>
<calendar-data>BEGIN:VCALENDAR${'\n'}X:twee</calendar-data>
</d:prop></d:propstat>
</d:response>
<d:response>
<d:propstat><d:prop>
<cal:calendar-data>BEGIN:VCALENDAR${'\n'}X:drie</cal:calendar-data>
</d:prop></d:propstat>
</d:response>
</d:multistatus>
""".trimIndent()
val blocks = Ical.extractCalendarBlocks(xml)
assertEquals(3, blocks.size)
assertEquals("BEGIN:VCALENDAR${'\n'}X:drie", blocks[2])
}
@Test
fun `DTSTART met TZID lost op naar lokale tijd in die zone`() {
val block = buildVEvent(
summary = "1234",
dtstart = "DTSTART;TZID=Europe/Amsterdam:20261015T050000",
)
val event = Ical.parseVEvent(block, amsterdam)!!
assertEquals(LocalDateTime.of(2026, 10, 15, 5, 0), event.start.toLocalDateTime())
assertEquals(amsterdam, event.start.zone)
assertEquals("1234", event.summary)
}
@Test
fun `DTSTART in UTC wordt omgerekend naar de lokale zone`() {
// 05:00 UTC op 15 juni = 07:00 zomertijd Amsterdam
val block = buildVEvent(
summary = "1234",
dtstart = "DTSTART:20260615T050000Z",
)
val event = Ical.parseVEvent(block, amsterdam)!!
val local = event.start.withZoneSameInstant(amsterdam)
assertEquals(LocalDateTime.of(2026, 6, 15, 7, 0), local.toLocalDateTime())
}
@Test
fun `all-day-event geeft geen diensttijd`() {
val block = buildVEvent(
summary = "vrij",
dtstart = "DTSTART;VALUE=DATE:20261015",
)
assertNull(Ical.parseVEvent(block, amsterdam))
}
@Test
fun `geannuleerd event wordt overgeslagen`() {
val block = buildVEvent(
summary = "1234",
dtstart = "DTSTART;TZID=Europe/Amsterdam:20261015T050000",
extra = "STATUS:CANCELLED",
)
assertNull(Ical.parseVEvent(block, amsterdam))
}
@Test
fun `eerste event is de vroegste aanvang`() {
val vcalendar = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Nextcloud//NEXTCLOUD//NL
BEGIN:VEVENT
UID:laat
SUMMARY:2222
DTSTART;TZID=Europe/Amsterdam:20261015T140000
DTEND;TZID=Europe/Amsterdam:20261015T210000
END:VEVENT
BEGIN:VEVENT
UID:vroeg
SUMMARY:1111
DTSTART;TZID=Europe/Amsterdam:20261015T043000
DTEND;TZID=Europe/Amsterdam:20261015T120000
END:VEVENT
END:VCALENDAR
""".trimIndent()
val event = Ical.firstTimedEvent(vcalendar)!!
assertEquals("1111", event.summary)
assertEquals(LocalDateTime.of(2026, 10, 15, 4, 30), event.start.toLocalDateTime())
}
@Test
fun `unfold voegt gevouwen regels samen`() {
// RFC 5545: CRLF + spatie/tab na de break is het vouwteken en wordt verwijderd.
val lines = Ical.unfold("SUMMARY:een \r\n lange samenvatting\r\nDTSTART:20260615T050000")
assertEquals("SUMMARY:een lange samenvatting", lines[0])
assertEquals("DTSTART:20260615T050000", lines[1])
}
@Test
fun `XML-ampex wordt ontsnapt in calendar-data`() {
val xml = ("<c:calendar-data>" +
"BEGIN:VCALENDAR\nBEGIN:VEVENT\nSUMMARY:Dienst &amp; meer\n" +
"DTSTART;TZID=Europe/Amsterdam:20261015T050000\nEND:VEVENT\nEND:VCALENDAR" +
"</c:calendar-data>")
val block = Ical.extractCalendarBlocks(xml).single()
val event = Ical.parseVEvent(block, amsterdam)!!
assertEquals("Dienst & meer", event.summary)
}
private fun buildVEvent(summary: String, dtstart: String, extra: String = ""): String {
val extraLine = if (extra.isBlank()) "" else "$extra\n"
return """
BEGIN:VEVENT
UID:test-1
SUMMARY:$summary
$dtstart
${extraLine}END:VEVENT
""".trimIndent() + "\n"
}
}

View file

@ -0,0 +1,68 @@
package nl.eddydevink.buschauffeurswekker.core
import java.time.LocalDateTime
import java.time.LocalTime
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class ShiftPlannerTest {
@Test
fun `afronding naar beneden op kwartier - vier gegeven voorbeelden`() {
// 05:00-05:14 -> 05:00
assertEquals(LocalTime.of(5, 0), ShiftPlanner.roundDownToQuarter(LocalTime.of(5, 0)))
assertEquals(LocalTime.of(5, 0), ShiftPlanner.roundDownToQuarter(LocalTime.of(5, 14)))
// 05:15-05:29 -> 05:15
assertEquals(LocalTime.of(5, 15), ShiftPlanner.roundDownToQuarter(LocalTime.of(5, 15)))
assertEquals(LocalTime.of(5, 15), ShiftPlanner.roundDownToQuarter(LocalTime.of(5, 29)))
// 05:30-05:44 -> 05:30
assertEquals(LocalTime.of(5, 30), ShiftPlanner.roundDownToQuarter(LocalTime.of(5, 30)))
assertEquals(LocalTime.of(5, 30), ShiftPlanner.roundDownToQuarter(LocalTime.of(5, 44)))
// 05:45-05:59 -> 05:45
assertEquals(LocalTime.of(5, 45), ShiftPlanner.roundDownToQuarter(LocalTime.of(5, 45)))
assertEquals(LocalTime.of(5, 45), ShiftPlanner.roundDownToQuarter(LocalTime.of(5, 59)))
}
@Test
fun `afronding randen en middernacht`() {
assertEquals(LocalTime.of(0, 0), ShiftPlanner.roundDownToQuarter(LocalTime.of(0, 0)))
assertEquals(LocalTime.of(0, 0), ShiftPlanner.roundDownToQuarter(LocalTime.of(0, 14)))
assertEquals(LocalTime.of(23, 45), ShiftPlanner.roundDownToQuarter(LocalTime.of(23, 59)))
// seconden/nanoseconden worden op nul gezet
assertEquals(
LocalTime.of(5, 30),
ShiftPlanner.roundDownToQuarter(LocalTime.of(5, 44, 59)),
)
}
@Test
fun `plan zet Opstaan Wally en Vertrek op de juiste offsets`() {
val shift = LocalDateTime.of(2026, 10, 15, 5, 10) // -> afgerond 05:00
val plan = ShiftPlanner.plan(shift)
assertEquals(3, plan.size)
assertEquals("Opstaan", plan[0].name)
assertEquals(LocalDateTime.of(2026, 10, 15, 3, 0), plan[0].time) // 2u voor 05:00
assertEquals("Wally", plan[1].name)
assertEquals(LocalDateTime.of(2026, 10, 15, 3, 30), plan[1].time) // 1,5u voor 05:00
assertEquals("Vertrek", plan[2].name)
assertEquals(LocalDateTime.of(2026, 10, 15, 4, 30), plan[2].time) // 0,5u voor 05:00
}
@Test
fun `plan rekent over middernacht heen`() {
val shift = LocalDateTime.of(2026, 10, 15, 0, 10) // -> afgerond 00:00
val plan = ShiftPlanner.plan(shift)
assertEquals(LocalDateTime.of(2026, 10, 14, 22, 0), plan[0].time) // Opstaan
assertEquals(LocalDateTime.of(2026, 10, 14, 22, 30), plan[1].time) // Wally
assertEquals(LocalDateTime.of(2026, 10, 14, 23, 30), plan[2].time) // Vertrek
}
@Test
fun `geen dienst geeft geen wekkers`() {
val plan = ShiftPlanner.plan(null)
assertTrue(plan.isEmpty())
}
}