Features: - Sleep tracking with timer and manual duration input - Statistics module with 5 tabs (daily summary, feeding, diapers, sleep, growth) - Export/Import backup functionality in settings - Complete German, French and Italian translations
344 lines
12 KiB
Kotlin
344 lines
12 KiB
Kotlin
package utils
|
|
|
|
import it.danieleverducci.lunatracker.entities.LunaEvent
|
|
import java.util.Calendar
|
|
|
|
/**
|
|
* Data classes for statistics results
|
|
*/
|
|
data class DailySummary(
|
|
val date: Long,
|
|
val totalBottleMl: Int,
|
|
val bottleCount: Int,
|
|
val totalBreastfeedingMin: Int,
|
|
val breastfeedingCount: Int,
|
|
val breastfeedingLeftCount: Int,
|
|
val breastfeedingRightCount: Int,
|
|
val totalSleepMin: Int,
|
|
val sleepCount: Int,
|
|
val diaperPooCount: Int,
|
|
val diaperPeeCount: Int,
|
|
val totalFoodCount: Int,
|
|
val latestWeight: Int?,
|
|
val latestTemperature: Int?
|
|
)
|
|
|
|
data class FeedingStats(
|
|
val dailyBottleTotals: Map<Long, Int>,
|
|
val dailyBreastfeedingTotals: Map<Long, Int>,
|
|
val avgBottleMlPerDay: Float,
|
|
val avgBreastfeedingMinPerDay: Float,
|
|
val leftBreastCount: Int,
|
|
val rightBreastCount: Int,
|
|
val bothBreastCount: Int,
|
|
val avgBreastfeedingDuration: Float,
|
|
val avgFeedingIntervalMinutes: Long
|
|
)
|
|
|
|
data class DiaperStats(
|
|
val dailyPooCount: Map<Long, Int>,
|
|
val dailyPeeCount: Map<Long, Int>,
|
|
val avgDiapersPerDay: Float,
|
|
val avgPooPerDay: Float,
|
|
val avgPeePerDay: Float,
|
|
val lastPooTime: Long?
|
|
)
|
|
|
|
data class SleepStats(
|
|
val dailyTotals: Map<Long, Int>,
|
|
val avgSleepMinPerDay: Float,
|
|
val avgNapsPerDay: Float,
|
|
val avgNapDurationMin: Float,
|
|
val longestSleepMin: Int,
|
|
val lastSleepTime: Long?
|
|
)
|
|
|
|
data class WeightPoint(
|
|
val time: Long,
|
|
val weightGrams: Int
|
|
)
|
|
|
|
data class TemperaturePoint(
|
|
val time: Long,
|
|
val temperatureDeciCelsius: Int
|
|
)
|
|
|
|
/**
|
|
* Calculator for statistics based on LunaEvent data
|
|
*/
|
|
class StatisticsCalculator(private val events: List<LunaEvent>) {
|
|
|
|
private fun getStartOfDay(unixTimeSeconds: Long): Long {
|
|
val cal = Calendar.getInstance()
|
|
cal.timeInMillis = unixTimeSeconds * 1000
|
|
cal.set(Calendar.HOUR_OF_DAY, 0)
|
|
cal.set(Calendar.MINUTE, 0)
|
|
cal.set(Calendar.SECOND, 0)
|
|
cal.set(Calendar.MILLISECOND, 0)
|
|
return cal.timeInMillis / 1000
|
|
}
|
|
|
|
private fun getEventsInRange(startUnix: Long, endUnix: Long): List<LunaEvent> {
|
|
return events.filter { it.time >= startUnix && it.time < endUnix }
|
|
}
|
|
|
|
private fun getEventsForDays(days: Int): List<LunaEvent> {
|
|
val now = System.currentTimeMillis() / 1000
|
|
val startOfToday = getStartOfDay(now)
|
|
val startTime = startOfToday - (days - 1) * 24 * 60 * 60
|
|
return events.filter { it.time >= startTime }
|
|
}
|
|
|
|
/**
|
|
* Get summary for a specific day (unix timestamp in seconds)
|
|
*/
|
|
fun getDailySummary(dayUnix: Long): DailySummary {
|
|
val startOfDay = getStartOfDay(dayUnix)
|
|
val endOfDay = startOfDay + 24 * 60 * 60
|
|
val dayEvents = getEventsInRange(startOfDay, endOfDay)
|
|
|
|
val bottleEvents = dayEvents.filter { it.type == LunaEvent.TYPE_BABY_BOTTLE }
|
|
val breastfeedingEvents = dayEvents.filter {
|
|
it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE ||
|
|
it.type == LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE ||
|
|
it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
|
|
}
|
|
val sleepEvents = dayEvents.filter { it.type == LunaEvent.TYPE_SLEEP }
|
|
val pooEvents = dayEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_POO }
|
|
val peeEvents = dayEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_PEE }
|
|
val foodEvents = dayEvents.filter { it.type == LunaEvent.TYPE_FOOD }
|
|
val weightEvents = dayEvents.filter { it.type == LunaEvent.TYPE_WEIGHT }
|
|
val tempEvents = dayEvents.filter { it.type == LunaEvent.TYPE_TEMPERATURE }
|
|
|
|
return DailySummary(
|
|
date = startOfDay,
|
|
totalBottleMl = bottleEvents.sumOf { it.quantity },
|
|
bottleCount = bottleEvents.size,
|
|
totalBreastfeedingMin = breastfeedingEvents.sumOf { it.quantity },
|
|
breastfeedingCount = breastfeedingEvents.size,
|
|
breastfeedingLeftCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE },
|
|
breastfeedingRightCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE },
|
|
totalSleepMin = sleepEvents.sumOf { it.quantity },
|
|
sleepCount = sleepEvents.size,
|
|
diaperPooCount = pooEvents.size,
|
|
diaperPeeCount = peeEvents.size,
|
|
totalFoodCount = foodEvents.size,
|
|
latestWeight = weightEvents.maxByOrNull { it.time }?.quantity,
|
|
latestTemperature = tempEvents.maxByOrNull { it.time }?.quantity
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Get today's summary
|
|
*/
|
|
fun getTodaySummary(): DailySummary {
|
|
return getDailySummary(System.currentTimeMillis() / 1000)
|
|
}
|
|
|
|
/**
|
|
* Get feeding statistics for the last N days
|
|
*/
|
|
fun getFeedingStats(days: Int): FeedingStats {
|
|
val relevantEvents = getEventsForDays(days)
|
|
val now = System.currentTimeMillis() / 1000
|
|
val startOfToday = getStartOfDay(now)
|
|
|
|
// Daily totals
|
|
val dailyBottleTotals = mutableMapOf<Long, Int>()
|
|
val dailyBreastfeedingTotals = mutableMapOf<Long, Int>()
|
|
|
|
for (i in 0 until days) {
|
|
val dayStart = startOfToday - i * 24 * 60 * 60
|
|
dailyBottleTotals[dayStart] = 0
|
|
dailyBreastfeedingTotals[dayStart] = 0
|
|
}
|
|
|
|
val bottleEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_BABY_BOTTLE }
|
|
val breastfeedingEvents = relevantEvents.filter {
|
|
it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE ||
|
|
it.type == LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE ||
|
|
it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
|
|
}
|
|
|
|
bottleEvents.forEach { event ->
|
|
val dayStart = getStartOfDay(event.time)
|
|
dailyBottleTotals[dayStart] = (dailyBottleTotals[dayStart] ?: 0) + event.quantity
|
|
}
|
|
|
|
breastfeedingEvents.forEach { event ->
|
|
val dayStart = getStartOfDay(event.time)
|
|
dailyBreastfeedingTotals[dayStart] = (dailyBreastfeedingTotals[dayStart] ?: 0) + event.quantity
|
|
}
|
|
|
|
// Breastfeeding side distribution
|
|
val leftCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE }
|
|
val rightCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE }
|
|
val bothCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE }
|
|
|
|
// Average breastfeeding duration
|
|
val avgBreastfeedingDuration = if (breastfeedingEvents.isNotEmpty()) {
|
|
breastfeedingEvents.sumOf { it.quantity }.toFloat() / breastfeedingEvents.size
|
|
} else 0f
|
|
|
|
// Average feeding interval (all feeding events sorted by time)
|
|
val allFeedingEvents = (bottleEvents + breastfeedingEvents).sortedBy { it.time }
|
|
val avgFeedingIntervalMinutes = if (allFeedingEvents.size > 1) {
|
|
var totalInterval = 0L
|
|
for (i in 1 until allFeedingEvents.size) {
|
|
totalInterval += allFeedingEvents[i].time - allFeedingEvents[i-1].time
|
|
}
|
|
(totalInterval / (allFeedingEvents.size - 1)) / 60
|
|
} else 0L
|
|
|
|
return FeedingStats(
|
|
dailyBottleTotals = dailyBottleTotals,
|
|
dailyBreastfeedingTotals = dailyBreastfeedingTotals,
|
|
avgBottleMlPerDay = if (days > 0) dailyBottleTotals.values.sum().toFloat() / days else 0f,
|
|
avgBreastfeedingMinPerDay = if (days > 0) dailyBreastfeedingTotals.values.sum().toFloat() / days else 0f,
|
|
leftBreastCount = leftCount,
|
|
rightBreastCount = rightCount,
|
|
bothBreastCount = bothCount,
|
|
avgBreastfeedingDuration = avgBreastfeedingDuration,
|
|
avgFeedingIntervalMinutes = avgFeedingIntervalMinutes
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Get diaper statistics for the last N days
|
|
*/
|
|
fun getDiaperStats(days: Int): DiaperStats {
|
|
val relevantEvents = getEventsForDays(days)
|
|
val now = System.currentTimeMillis() / 1000
|
|
val startOfToday = getStartOfDay(now)
|
|
|
|
val dailyPooCount = mutableMapOf<Long, Int>()
|
|
val dailyPeeCount = mutableMapOf<Long, Int>()
|
|
|
|
for (i in 0 until days) {
|
|
val dayStart = startOfToday - i * 24 * 60 * 60
|
|
dailyPooCount[dayStart] = 0
|
|
dailyPeeCount[dayStart] = 0
|
|
}
|
|
|
|
val pooEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_POO }
|
|
val peeEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_PEE }
|
|
|
|
pooEvents.forEach { event ->
|
|
val dayStart = getStartOfDay(event.time)
|
|
dailyPooCount[dayStart] = (dailyPooCount[dayStart] ?: 0) + 1
|
|
}
|
|
|
|
peeEvents.forEach { event ->
|
|
val dayStart = getStartOfDay(event.time)
|
|
dailyPeeCount[dayStart] = (dailyPeeCount[dayStart] ?: 0) + 1
|
|
}
|
|
|
|
val totalDiapers = pooEvents.size + peeEvents.size
|
|
|
|
return DiaperStats(
|
|
dailyPooCount = dailyPooCount,
|
|
dailyPeeCount = dailyPeeCount,
|
|
avgDiapersPerDay = if (days > 0) totalDiapers.toFloat() / days else 0f,
|
|
avgPooPerDay = if (days > 0) pooEvents.size.toFloat() / days else 0f,
|
|
avgPeePerDay = if (days > 0) peeEvents.size.toFloat() / days else 0f,
|
|
lastPooTime = pooEvents.maxByOrNull { it.time }?.time
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Get sleep statistics for the last N days
|
|
*/
|
|
fun getSleepStats(days: Int): SleepStats {
|
|
val relevantEvents = getEventsForDays(days)
|
|
val now = System.currentTimeMillis() / 1000
|
|
val startOfToday = getStartOfDay(now)
|
|
|
|
val dailyTotals = mutableMapOf<Long, Int>()
|
|
|
|
for (i in 0 until days) {
|
|
val dayStart = startOfToday - i * 24 * 60 * 60
|
|
dailyTotals[dayStart] = 0
|
|
}
|
|
|
|
val sleepEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_SLEEP }
|
|
|
|
sleepEvents.forEach { event ->
|
|
val dayStart = getStartOfDay(event.time)
|
|
dailyTotals[dayStart] = (dailyTotals[dayStart] ?: 0) + event.quantity
|
|
}
|
|
|
|
val totalSleepMin = sleepEvents.sumOf { it.quantity }
|
|
val avgNapDuration = if (sleepEvents.isNotEmpty()) {
|
|
totalSleepMin.toFloat() / sleepEvents.size
|
|
} else 0f
|
|
|
|
val longestSleep = sleepEvents.maxOfOrNull { it.quantity } ?: 0
|
|
|
|
return SleepStats(
|
|
dailyTotals = dailyTotals,
|
|
avgSleepMinPerDay = if (days > 0) totalSleepMin.toFloat() / days else 0f,
|
|
avgNapsPerDay = if (days > 0) sleepEvents.size.toFloat() / days else 0f,
|
|
avgNapDurationMin = avgNapDuration,
|
|
longestSleepMin = longestSleep,
|
|
lastSleepTime = sleepEvents.maxByOrNull { it.time }?.time
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Get weight history (all weight measurements)
|
|
*/
|
|
fun getWeightHistory(): List<WeightPoint> {
|
|
return events
|
|
.filter { it.type == LunaEvent.TYPE_WEIGHT && it.quantity > 0 }
|
|
.sortedBy { it.time }
|
|
.map { WeightPoint(it.time, it.quantity) }
|
|
}
|
|
|
|
/**
|
|
* Get temperature history
|
|
*/
|
|
fun getTemperatureHistory(): List<TemperaturePoint> {
|
|
return events
|
|
.filter { it.type == LunaEvent.TYPE_TEMPERATURE && it.quantity > 0 }
|
|
.sortedBy { it.time }
|
|
.map { TemperaturePoint(it.time, it.quantity) }
|
|
}
|
|
|
|
/**
|
|
* Calculate weight gain over the last N days
|
|
*/
|
|
fun getWeightGainForDays(days: Int): Int? {
|
|
val weights = getWeightHistory()
|
|
if (weights.size < 2) return null
|
|
|
|
val now = System.currentTimeMillis() / 1000
|
|
val startTime = now - days * 24 * 60 * 60
|
|
|
|
val recentWeight = weights.lastOrNull() ?: return null
|
|
val olderWeight = weights.filter { it.time <= startTime }.lastOrNull()
|
|
?: weights.firstOrNull()
|
|
?: return null
|
|
|
|
if (recentWeight.time == olderWeight.time) return null
|
|
|
|
return recentWeight.weightGrams - olderWeight.weightGrams
|
|
}
|
|
|
|
/**
|
|
* Get average daily values for a type of event over N days
|
|
*/
|
|
fun getAverageDailyCount(type: String, days: Int): Float {
|
|
val relevantEvents = getEventsForDays(days).filter { it.type == type }
|
|
return if (days > 0) relevantEvents.size.toFloat() / days else 0f
|
|
}
|
|
|
|
/**
|
|
* Get average daily quantity sum for a type of event over N days
|
|
*/
|
|
fun getAverageDailyQuantity(type: String, days: Int): Float {
|
|
val relevantEvents = getEventsForDays(days).filter { it.type == type }
|
|
val total = relevantEvents.sumOf { it.quantity }
|
|
return if (days > 0) total.toFloat() / days else 0f
|
|
}
|
|
}
|