package no.naiv.meddetsamme.domain import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.Locale import no.naiv.meddetsamme.data.DoseTime /** * Human-readable schedule descriptions (Norwegian, 24h) for the doctor * summary and the UI. Pure — unit-tested like the engine, since a wrong * description on a doctor's desk is a clinical communication error. */ object ScheduleText { /** Mask bit order matches ScheduleEngine: bit 0 = Sunday. */ private val DAY_ABBREV = listOf("søn", "man", "tir", "ons", "tor", "fre", "lør") // Locale-pinned to nb so month names don't come out English on a foreign locale. private val dateFmt = DateTimeFormatter.ofPattern("d. MMM", Locale.forLanguageTag("nb")) fun describe(doseTime: DoseTime): String = "${"%02d:%02d".format(doseTime.minuteOfDay / 60, doseTime.minuteOfDay % 60)} " + "${daysText(doseTime)}${cycleText(doseTime)}${windowText(doseTime)}${seasonText(doseTime)}" /** " · syklus 21/28" when cyclic, else "". */ fun cycleText(doseTime: DoseTime): String { val length = doseTime.cycleLengthDays val active = doseTime.cycleActiveDays return if (length > 0 && active in 1 until length) " · syklus $active/$length" else "" } /** " · 1.–14. jun", " · f.o.m. 1. jun", " · t.o.m. 14. jun" for a validity window. */ fun windowText(doseTime: DoseTime): String { val start = doseTime.startEpochDay?.let { LocalDate.ofEpochDay(it) } val end = doseTime.endEpochDay?.let { LocalDate.ofEpochDay(it) } return when { start != null && end != null -> " · ${dateFmt.format(start)}–${dateFmt.format(end)}" start != null -> " · f.o.m. ${dateFmt.format(start)}" end != null -> " · t.o.m. ${dateFmt.format(end)}" else -> "" } } /** " · sesong 1. mar–31. mai" for a yearly seasonal window (month×100+day). */ fun seasonText(doseTime: DoseTime): String { val start = doseTime.seasonStartMmdd val end = doseTime.seasonEndMmdd if (start <= 0 || end <= 0) return "" return " · sesong ${mmddText(start)}–${mmddText(end)}" } /** Formats a month×100+day mark as "1. mar". A leap-year-safe MonthDay so 0229 renders. */ private fun mmddText(mmdd: Int): String = java.time.MonthDay.of(mmdd / 100, mmdd % 100).format(dateFmt) /** A single date as "1. mar" (nb-pinned), e.g. for "next season starts …". */ fun dateLabel(date: LocalDate): String = dateFmt.format(date) fun daysText(doseTime: DoseTime): String { if (doseTime.intervalDays > 1) { return "(hver ${doseTime.intervalDays}. dag)" } return when (val mask = doseTime.daysOfWeekMask and 0x7F) { 0x7F -> "(daglig)" 0x3E -> "(man–fre)" 0x41 -> "(lør–søn)" 0 -> "(aldri)" else -> { // Week listed Monday-first for humans, despite bit 0 = Sunday. val days = (1..7).map { it % 7 } .filter { mask and (1 shl it) != 0 } .map { DAY_ABBREV[it] } "(${days.joinToString(", ")})" } } } /** "1 dag", "3 dager" — norsk flertall. */ fun daysCount(days: Int): String = if (days == 1) "1 dag" else "$days dager" /** "1 tablett", "2,5 ml" — norsk desimalkomma. */ fun amountText(amount: Double, unit: String): String { val number = if (amount == amount.toLong().toDouble()) { amount.toLong().toString() } else { amount.toString().replace('.', ',') } return "$number $unit" } }