summaryrefslogtreecommitdiff
path: root/android/app/src/main/java/org/terst/doot/widget/data/SlotPacker.kt
blob: da388ded4e8d154336e32a2ee9d5cea7e22a7788 (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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package org.terst.doot.widget.data

import java.time.Instant
import java.time.temporal.ChronoUnit

data class FloatingSlot(
    val task: WidgetItem,
    val slotStart: Instant,
    val slotEnd: Instant
)

data class TaskFragment(
    val slots: List<FloatingSlot>,
    val startTime: Instant,
    val endTime: Instant
)

object SlotPacker {
    /**
     * Assigns 1-hour slots to floating tasks, packing them forward from [now],
     * bumping past any overlapping scheduled events.
     * Returns groups of consecutive task slots as TaskFragments.
     * A new fragment starts whenever a scheduled event falls between two task slots.
     */
    fun pack(
        floatingTasks: List<WidgetItem>,
        scheduledEvents: List<WidgetItem>,
        now: Instant
    ): List<TaskFragment> {
        if (floatingTasks.isEmpty()) return emptyList()

        val eventIntervals: List<Pair<Instant, Instant>> = scheduledEvents.mapNotNull { event ->
            val s = event.start?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return@mapNotNull null
            val e = event.end?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return@mapNotNull null
            s to e
        }

        val slots = mutableListOf<FloatingSlot>()
        var cursor = now

        for (task in floatingTasks) {
            var slotStart = cursor
            var slotEnd = slotStart.plus(1, ChronoUnit.HOURS)

            // Bump cursor past any overlapping event, re-checking after each bump
            var bumped = true
            while (bumped) {
                bumped = false
                for ((evStart, evEnd) in eventIntervals) {
                    if (slotStart < evEnd && slotEnd > evStart) {
                        slotStart = evEnd
                        slotEnd = slotStart.plus(1, ChronoUnit.HOURS)
                        bumped = true
                        break
                    }
                }
            }

            slots.add(FloatingSlot(task, slotStart, slotEnd))
            cursor = slotEnd
        }

        // Group consecutive slots into fragments, splitting when a scheduled event falls between them
        val fragments = mutableListOf<TaskFragment>()
        var current = mutableListOf(slots[0])

        for (i in 1 until slots.size) {
            val prevEnd = slots[i - 1].slotEnd
            val nextStart = slots[i].slotStart
            val eventBetween = prevEnd < nextStart && eventIntervals.any { (evStart, _) ->
                evStart >= prevEnd && evStart < nextStart
            }
            if (eventBetween) {
                fragments.add(current.toFragment())
                current = mutableListOf()
            }
            current.add(slots[i])
        }
        fragments.add(current.toFragment())

        return fragments
    }

    private fun List<FloatingSlot>.toFragment() = TaskFragment(
        slots = this,
        startTime = first().slotStart,
        endTime = last().slotEnd
    )
}