Buschauffeurswekker v1: CalDAV-dagcheck 12:00, kwartier-afronding, wekkers Opstaan/Wally/Vertrek, fullscreen alarm-UI
This commit is contained in:
parent
97b7d3090e
commit
34505be9b8
31 changed files with 1881 additions and 1 deletions
|
|
@ -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 & 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue