References are not forever: a park is delisted, a district merged, a castle loses its number. A QSO made while the reference existed is a valid contact and must keep counting; one made afterwards must not. The JSON format did not need to change — ValidFrom/ValidTo were already on awardref.Ref, already columns in award_references, already round-tripping through export and import. They were simply never read: awardRefMetas dropped them on the way into the engine, so nothing downstream could enforce them. This carries them through and checks them in keepRefs. Empty means the award's own window governs, which inScope already enforces for every QSO in the award. That fallback is deliberately NOT duplicated per reference — two places enforcing the same dates is two places for them to disagree. The editor shows the award's dates as the hint under the boxes so the operator can see what empty inherits. Dates are compared as ISO strings rather than parsed times: the stored shape is "2006-01-02", lexical order on it IS chronological, and this cannot fail on a malformed value the way a parse can — a reference with a typo in its window keeps counting instead of silently vanishing from an operator's totals. The Explain trace names the date and the cutoff, because "did not count" on a contact the operator remembers making is exactly when they need to be told it is the reference that has a window, not their log that is wrong.
79 lines
2.5 KiB
Go
79 lines
2.5 KiB
Go
package award
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"hamlog/internal/qso"
|
|
)
|
|
|
|
func day(s string) time.Time {
|
|
t, err := time.Parse("2006-01-02", s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return t
|
|
}
|
|
|
|
// A reference is not forever. A park is delisted, a district is merged, a castle
|
|
// loses its number. A contact made while it existed still counts — it was a
|
|
// valid contact on the day — and one made after it stopped existing does not.
|
|
func TestRefValidityWindow(t *testing.T) {
|
|
m := RefMeta{Code: "KL-01", Valid: true, ValidTo: "2025-08-31"}
|
|
for _, tc := range []struct {
|
|
day string
|
|
want bool
|
|
}{
|
|
{"2019-01-01", true},
|
|
{"2025-08-31", true}, // the last day it existed still counts
|
|
{"2025-09-01", false},
|
|
{"2026-08-12", false},
|
|
} {
|
|
if got := m.activeOn(tc.day); got != tc.want {
|
|
t.Errorf("KL-01 on %s: active=%v, want %v", tc.day, got, tc.want)
|
|
}
|
|
}
|
|
|
|
// A reference that only came into being partway through.
|
|
n := RefMeta{Code: "KL-99", Valid: true, ValidFrom: "2025-01-15"}
|
|
if n.activeOn("2025-01-14") {
|
|
t.Error("counted a QSO from before the reference existed")
|
|
}
|
|
if !n.activeOn("2025-01-15") {
|
|
t.Error("the first day it existed must count")
|
|
}
|
|
|
|
// No window of its own: the award's own dates govern, as they already do for
|
|
// every QSO in the award. Nothing here may narrow that.
|
|
if !(RefMeta{Code: "X", Valid: true}).activeOn("1970-01-01") {
|
|
t.Error("a reference with no window must count on any date")
|
|
}
|
|
}
|
|
|
|
// The whole point: the same QSO counts before the cutoff and does not after.
|
|
func TestExpiredRefStopsCountingForLaterQSOs(t *testing.T) {
|
|
d := &Def{
|
|
Code: "RDA", Name: "Russian District Award", Valid: true,
|
|
Type: TypeQSOFields, Field: "note", MatchBy: "code",
|
|
Confirm: []string{"lotw"},
|
|
}
|
|
metas := []RefMeta{
|
|
{Code: "KL-01", Name: "Petrozavodsk", Valid: true, ValidTo: "2025-08-31"},
|
|
{Code: "KL-04", Name: "Kostomuksha", Valid: true},
|
|
}
|
|
|
|
q := func(ref, on string) *qso.QSO {
|
|
return &qso.QSO{Callsign: "RA1ABC", Band: "20m", Notes: ref, QSODate: day(on)}
|
|
}
|
|
|
|
if got := MatchQSO(*d, metas, q("KL-01", "2025-06-01")); len(got) != 1 || got[0] != "KL-01" {
|
|
t.Errorf("a QSO made while KL-01 existed must count: got %v", got)
|
|
}
|
|
if got := MatchQSO(*d, metas, q("KL-01", "2025-09-15")); len(got) != 0 {
|
|
t.Errorf("a QSO made after KL-01 ceased to exist must not count: got %v", got)
|
|
}
|
|
if got := MatchQSO(*d, metas, q("KL-04", "2025-09-15")); len(got) != 1 || got[0] != "KL-04" {
|
|
t.Errorf("a reference with no window is unaffected: got %v", got)
|
|
}
|
|
}
|