2026-06-10 14:02:34 +02:00
|
|
|
|
package no.naiv.meddetsamme.domain
|
|
|
|
|
|
|
2026-06-11 22:09:51 +02:00
|
|
|
|
import java.time.LocalDate
|
|
|
|
|
|
import java.time.format.DateTimeFormatter
|
|
|
|
|
|
import java.util.Locale
|
2026-06-10 14:02:34 +02:00
|
|
|
|
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")
|
|
|
|
|
|
|
2026-06-11 22:09:51 +02:00
|
|
|
|
// 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)} " +
|
2026-06-24 11:09:46 +02:00
|
|
|
|
"${daysText(doseTime)}${cycleText(doseTime)}${windowText(doseTime)}${seasonText(doseTime)}"
|
2026-06-11 22:09:51 +02:00
|
|
|
|
|
|
|
|
|
|
/** " · 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 -> ""
|
|
|
|
|
|
}
|
2026-06-10 14:02:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 11:09:46 +02:00
|
|
|
|
/** " · 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)
|
|
|
|
|
|
|
2026-06-10 14:02:34 +02:00
|
|
|
|
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(", ")})"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 17:09:07 +02:00
|
|
|
|
/** "1 dag", "3 dager" — norsk flertall. */
|
|
|
|
|
|
fun daysCount(days: Int): String = if (days == 1) "1 dag" else "$days dager"
|
|
|
|
|
|
|
2026-06-10 14:02:34 +02:00
|
|
|
|
/** "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"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|