What the WCA rules require (why these changes matter)
• Average of 5 (Ao5): remove the best and worst; one DNF/DNS may count as the worst; with 2+ DNF/DNS the average is DNF. 
• Mean of 3 (Mo3): any DNF/DNS ⇒ mean is DNF. 
• Rounding: attempts are recorded to hundredths under 10:00; averages/means are rounded to nearest hundredth under 10:00 and to the nearest second above 10:00. (This is easy to overlook!) 
Review of your code
• ✅ Logic for Ao5 with 0 or 1 DNF is conceptually right, and Mo3 returns DNF if any null—good.
• ⚠️ Rounding not applied per WCA (nearest 0.01s under 10:00; nearest second ≥ 10:00).
• ⚠️ Multiple passes over the list (sum, min, max)—fine for 5 items, but trivial to make single-pass.
• ⚠️ Implicit size assumptions. solveAverage() should assert exactly 5 attempts; solveMean() should assert 3 attempts, otherwise unexpected input can sneak through.
• ⚠️ Floating-point drift. Using Double math for 0.01s rounding can bite (e.g., 1.23*100 ≈ 122.999…). Prefer integer centiseconds or BigDecimal.
• ⚠️ Validation. Guard against negatives / NaN.
• 🤏 Readability: explicit names like averageAo5 / meanMo3 make intent clear and encode the rule in the signature.
A tight, robust implementation (Kotlin 2.2.x)
This version:
• Uses centiseconds (cs) for arithmetic to avoid FP error.
• Does one pass to compute sum/min/max and DNF count.
• Applies the exact WCA rounding rules to the final average/mean.
• Enforces expected attempt counts.
```kt
private const val EPS = 1e-9
private fun toCentiseconds(timeSec: Double): Long {
require(timeSec >= 0.0 && !timeSec.isNaN()) { "Time must be a non-negative number" }
// WCA attempts are truncated to hundredths under 10:00; if your inputs already are,
// this preserves them. EPS avoids 122.999... from 1.23 * 100.
return kotlin.math.floor(timeSec * 100.0 + EPS).toLong()
}
/** Round a total (in centiseconds) divided by n attempts per WCA 9f1. */
private fun roundAverage(totalCs: Long, n: Int): Double {
// If the (true) average >= 600s, round to nearest second; else nearest hundredth.
val threshold = 60000L * n // 600s * 100cs/s * n
return if (totalCs >= threshold) {
// nearest second: divide by (n*100), half-up
val sec = (totalCs + (n * 100) / 2) / (n * 100)
sec.toDouble()
} else {
// nearest hundredth: divide by n cs, half-up
val cs = (totalCs + n / 2) / n
cs / 100.0
}
}
fun List<Double?>.averageAo5(): Double? {
require(size == 5) { "Average of 5 requires exactly 5 attempts." }
var dnfs = 0
var sumCs = 0L
var minCs = Long.MAX_VALUE
var maxCs = Long.MIN_VALUE
for (t in this) {
if (t == null) {
dnfs++
continue
}
val cs = toCentiseconds(t)
sumCs += cs
if (cs < minCs) minCs = cs
if (cs > maxCs) maxCs = cs
}
if (dnfs >= 2) return null // 9f9
val usedTotalCs = if (dnfs == 0) {
// remove best and worst
sumCs - minCs - maxCs
} else {
// one DNF counts as worst; remove best only
sumCs - minCs
}
// average over 3 results (size - 2)
return roundAverage(usedTotalCs, 3)
}
fun List<Double?>.meanMo3(): Double? {
require(size == 3) { "Mean of 3 requires exactly 3 attempts." }
var sumCs = 0L
for (t in this) {
if (t == null) return null // 9f11
sumCs += toCentiseconds(t)
}
return roundAverage(sumCs, 3)
}
```
Notes & optional tweaks
• If you also want to represent +2 penalties, keep your Double inputs as “final times including any penalties”. If you want stronger typing, model attempts as a sealed type (Time, DNF, DNS) and map to nullable Double at the edges.
• If you prefer BigDecimal over integer centiseconds, use it only at the final rounding step (it’s heavier than ints but fine for 3–5 items).
• If these run in a hot path, you can mark helpers @JvmInline value classes for cs/seconds, but for Ao5/Mo3 it won’t matter.
Why this is “correct”
• Ao5 keep/drop rules and DNF handling match 9f8–9f9; Mo3 DNF handling matches 9f10–9f11; rounding matches 9f1 (nearest 0.01s under 10:00; nearest second ≥ 10:00). 
If you want, toss me a couple real input examples (including a ≥10:00 case and a “with +2” case) and I’ll sanity-check the outputs against the regs.