package com.vgmlr.kiln
import java.time.LocalDate
import java.time.YearMonth
import java.time.temporal.ChronoUnit
private data class MoodTally(
val top: Int?,
val topCount: Int,
val second: Int?,
val secondCount: Int,
val total: Int
)
data class KilnDayStats(
val daysToPeriod: Long?,
val daysToPreMenstrual: Long?,
val topMood: Int?,
val topMoodCount: Int,
val secondMood: Int?,
val secondMoodCount: Int,
val moodDays: Int,
val intermissionAvg: Float,
val preMenstrualAvg: Float?,
val menstruationAvg: Float?
) {
companion object {
private fun scanSpan(cycle: Int) = cycle.toLong() + 1
fun of(
date: LocalDate,
today: LocalDate,
records: List<KilnDayRecord>,
buttons: KilnButtonSet,
starts: List<LocalDate>,
cycle: Int,
hueOf: (LocalDate) -> Float,
preMenstrualAvg: Float?
): KilnDayStats {
val moods = moodTally(records, YearMonth.from(date))
return KilnDayStats(
daysToPeriod = daysToPeriod(today, starts, cycle),
daysToPreMenstrual =
if (starts.isEmpty()) null else preMenstrualIn(today, cycle, hueOf),
topMood = moods.top,
topMoodCount = moods.topCount,
secondMood = moods.second,
secondMoodCount = moods.secondCount,
moodDays = moods.total,
intermissionAvg = KilnCyclePredictor.avgCycleExact(starts),
preMenstrualAvg = preMenstrualAvg,
menstruationAvg = KilnCyclePredictor.avgPeriodLength(records, buttons)
)
}
fun preMenstrualSeries(
starts: List<LocalDate>,
cycle: Int,
hueOf: (LocalDate) -> Float
): List<Pair<LocalDate, Int>> =
KilnCyclePredictor.measurableStarts(starts)
.mapNotNull { s -> leadInto(s, cycle, hueOf)?.let { s to it.toInt() } }
fun preMenstrualAvg(
starts: List<LocalDate>,
cycle: Int,
hueOf: (LocalDate) -> Float
): Float? {
val leads = preMenstrualSeries(starts, cycle, hueOf).map { it.second }
return if (leads.isEmpty()) null else leads.average().toFloat()
}
private fun leadInto(start: LocalDate, cycle: Int, hueOf: (LocalDate) -> Float): Long? {
if (!KilnGradientPalette.isPreMenstrual(hueOf(start))) return null
for (i in 1L..scanSpan(cycle)) {
if (!KilnGradientPalette.isPreMenstrual(hueOf(start.minusDays(i)))) return i - 1
}
return null
}
fun daysToPeriod(date: LocalDate, starts: List<LocalDate>, cycle: Int): Long? {
if (starts.isEmpty()) return null
starts.firstOrNull { !it.isBefore(date) }
?.let { return ChronoUnit.DAYS.between(date, it) }
val last = starts.last()
val gap = ChronoUnit.DAYS.between(last, date)
val k = (gap + cycle - 1) / cycle
return ChronoUnit.DAYS.between(date, last.plusDays(k * cycle))
}
fun preMenstrualIn(
date: LocalDate,
cycle: Int,
hueOf: (LocalDate) -> Float
): Long? {
var prev = KilnGradientPalette.isPreMenstrual(hueOf(date.minusDays(1)))
for (i in 0L..scanSpan(cycle)) {
val now = KilnGradientPalette.isPreMenstrual(hueOf(date.plusDays(i)))
if (now && !prev) return i
prev = now
}
return null
}
fun preMenstrualIn(
date: LocalDate,
records: List<KilnDayRecord>,
buttons: KilnButtonSet
): Long? {
val starts = KilnCyclePredictor.periodStarts(records, buttons)
if (starts.isEmpty()) return null
val cycle = KilnCyclePredictor.avgCycleLength(starts)
return preMenstrualIn(
date, cycle, KilnGradientPalette.lookup(records, buttons, starts, cycle)
)
}
private fun moodTally(records: List<KilnDayRecord>, month: YearMonth): MoodTally {
val counts = IntArray(5)
var total = 0
for (r in records) {
val m = r.mood ?: continue
if (m !in counts.indices) continue
if (YearMonth.from(LocalDate.ofEpochDay(r.epochDay)) != month) continue
counts[m]++
total++
}
if (total == 0) return MoodTally(null, 0, null, 0, 0)
val ranked = counts.indices.filter { counts[it] > 0 }
.sortedByDescending { counts[it] }
val top = ranked[0]
val second = ranked.getOrNull(1)
return MoodTally(top, counts[top], second, second?.let { counts[it] } ?: 0, total)
}
}
}