feat: added extra stats for contests
This commit is contained in:
@@ -57,7 +57,7 @@ func TestStatsNoNilSlices(t *testing.T) {
|
||||
// The realistic trigger: a short, gap-free run. No silence ≥ 30 min and a
|
||||
// window that yields no hourly chart must still produce [] and not null.
|
||||
base := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC)
|
||||
times := []time.Time{base, base.Add(2 * time.Minute), base.Add(5 * time.Minute)}
|
||||
times := []entry{{t: base, op: "A"}, {t: base.Add(2 * time.Minute), op: "A"}, {t: base.Add(5 * time.Minute), op: "B"}}
|
||||
var s2 Stats
|
||||
s2.periodMetrics(times, time.Time{}, time.Time{}, base, base.Add(5*time.Minute))
|
||||
s2.ensureNonNil()
|
||||
@@ -82,12 +82,12 @@ func TestContestPeriodMetrics(t *testing.T) {
|
||||
|
||||
// A run straddling the clock hour: 10 QSOs from 12:40 to 13:20 (within 60 min),
|
||||
// then a 2-hour silence, then 3 more.
|
||||
var times []time.Time
|
||||
var times []entry
|
||||
for i := 0; i < 10; i++ {
|
||||
times = append(times, at(40+i*4)) // 12:40 … 13:16
|
||||
times = append(times, entry{t: at(40 + i*4), op: "F4BPO"}) // 12:40 … 13:16
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
times = append(times, at(240+i*5)) // 16:00 …
|
||||
times = append(times, entry{t: at(240 + i*5), op: "F5XYZ"}) // 16:00 …
|
||||
}
|
||||
|
||||
from := base // 12:00
|
||||
@@ -139,6 +139,51 @@ func TestContestPeriodMetrics(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The contest RATE SHEET: hour by hour, who made the QSOs.
|
||||
//
|
||||
// The invariant that matters: for EVERY hour, the per-operator numbers must sum to
|
||||
// that hour's total. Derive the two separately and a rate sheet quietly stops
|
||||
// adding up to its own total row — the sort of error nobody spots until someone
|
||||
// checks the score by hand.
|
||||
func TestRateSheetSumsToHourTotal(t *testing.T) {
|
||||
base := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC)
|
||||
at := func(min int) time.Time { return base.Add(time.Duration(min) * time.Minute) }
|
||||
|
||||
times := []entry{
|
||||
{t: at(5), op: "F4BPO"}, {t: at(10), op: "F4BPO"}, {t: at(20), op: "F5XYZ"}, // hour 12: 3
|
||||
{t: at(70), op: "F5XYZ"}, {t: at(80), op: "F5XYZ"}, // hour 13: 2
|
||||
// hour 14 silent
|
||||
{t: at(185), op: "F4BPO"}, // hour 15: 1
|
||||
}
|
||||
var s Stats
|
||||
s.periodMetrics(times, base, base.Add(4*time.Hour), time.Time{}, time.Time{})
|
||||
|
||||
if len(s.Rate) != len(s.RateByOp) {
|
||||
t.Fatalf("rate rows (%d) and rate-sheet rows (%d) must align 1:1", len(s.Rate), len(s.RateByOp))
|
||||
}
|
||||
// Both operators present, busiest first (they tie at 3 → alphabetical).
|
||||
if len(s.RateOps) != 2 || s.RateOps[0] != "F4BPO" {
|
||||
t.Fatalf("rate ops = %v, want [F4BPO F5XYZ]", s.RateOps)
|
||||
}
|
||||
for h := range s.Rate {
|
||||
sum := 0
|
||||
for _, n := range s.RateByOp[h] {
|
||||
sum += n
|
||||
}
|
||||
if sum != s.Rate[h].Count {
|
||||
t.Errorf("hour %s: operators sum to %d but the hour total is %d — the rate sheet doesn't add up",
|
||||
s.Rate[h].Key, sum, s.Rate[h].Count)
|
||||
}
|
||||
if len(s.RateByOp[h]) != len(s.RateOps) {
|
||||
t.Errorf("hour %s: row has %d columns, want %d (one per operator)", s.Rate[h].Key, len(s.RateByOp[h]), len(s.RateOps))
|
||||
}
|
||||
}
|
||||
// The silent hour is a row of zeros, not a missing row.
|
||||
if s.Rate[2].Count != 0 || s.RateByOp[2][0] != 0 || s.RateByOp[2][1] != 0 {
|
||||
t.Errorf("the silent 14:00 hour must be zeros, got total=%d row=%v", s.Rate[2].Count, s.RateByOp[2])
|
||||
}
|
||||
}
|
||||
|
||||
// A quiet decade must appear on the trend as a decade AT ZERO. Emitting only the
|
||||
// months that have QSOs would put 2012 next to 2022 as if consecutive — the chart
|
||||
// would invent activity that never happened.
|
||||
|
||||
Reference in New Issue
Block a user