Pith - kiln
kiln/app/src/main/java/com/vgmlr/kiln/KilnDayStats.kt [4.9 kb]
Modified: 00:02:27 153 026 (19 Aug 026)
5 Days Ago
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)
        }
    }
}
Updates
Kerf - Android 157.026
Kiln - Android 157.026
Wedge - Android 156.026
Whittle - Linux 155.026
Grit - Firefox/Chrome 154.026

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)
0.00323
256,919 (+173)