package com.vgmlr.kiln
import java.time.LocalDate
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
object KilnCyclePredictor {
const val DEFAULT_CYCLE = 28
const val RUN_GAP = 3L
private val PLAUSIBLE = 15L..60L
fun periodStarts(records: List<KilnDayRecord>, buttons: KilnButtonSet): List<LocalDate> {
val days = records.filter { buttons.isPeriodSighting(it) }
.map { LocalDate.ofEpochDay(it.epochDay) }
.sorted()
val starts = mutableListOf<LocalDate>()
var prev: LocalDate? = null
for (d in days) {
if (prev == null || ChronoUnit.DAYS.between(prev, d) > RUN_GAP) starts += d
prev = d
}
return starts
}
fun avgCycleExact(starts: List<LocalDate>): Float {
val diffs = starts.zipWithNext { a, b -> ChronoUnit.DAYS.between(a, b) }
.filter { it in PLAUSIBLE }
if (diffs.isEmpty()) return DEFAULT_CYCLE.toFloat()
return (DEFAULT_CYCLE + diffs.sum()).toFloat() / (diffs.size + 1)
}
fun avgCycleLength(starts: List<LocalDate>): Int = avgCycleExact(starts).roundToInt()
fun measurableStarts(starts: List<LocalDate>): List<LocalDate> =
starts.zipWithNext { a, b -> b.takeIf { ChronoUnit.DAYS.between(a, b) in PLAUSIBLE } }
.filterNotNull()
fun cycleSeries(starts: List<LocalDate>): List<Pair<LocalDate, Int>> =
starts.zipWithNext { a, b ->
val d = ChronoUnit.DAYS.between(a, b)
if (d in PLAUSIBLE) b to d.toInt() else null
}.filterNotNull()
fun periodRuns(
records: List<KilnDayRecord>,
buttons: KilnButtonSet
): List<Pair<LocalDate, Int>> {
val days = records.filter { buttons.isPeriodSighting(it) }.map { it.epochDay }.sorted()
val runs = mutableListOf<Pair<LocalDate, Int>>()
var start = days.firstOrNull() ?: return runs
var prev = start
for (d in days.drop(1)) {
if (d - prev > RUN_GAP) {
runs += LocalDate.ofEpochDay(start) to (prev - start + 1).toInt()
start = d
}
prev = d
}
runs += LocalDate.ofEpochDay(start) to (prev - start + 1).toInt()
return runs
}
fun periodLengths(records: List<KilnDayRecord>, buttons: KilnButtonSet): List<Int> =
periodRuns(records, buttons).map { it.second }
fun avgPeriodLength(records: List<KilnDayRecord>, buttons: KilnButtonSet): Float? {
val lengths = periodLengths(records, buttons)
return if (lengths.isEmpty()) null else lengths.average().toFloat()
}
}