Add sleep tracking, statistics module and backup features
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
This commit is contained in:
343
app/src/main/java/utils/StatisticsCalculator.kt
Normal file
343
app/src/main/java/utils/StatisticsCalculator.kt
Normal file
@@ -0,0 +1,343 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user