feat(log): QSO number column, oldest contact = 1
Not the id: the primary key follows insertion order, so importing an old ADIF gives the oldest contacts the highest ids. This is a rank over qso_date. Computed over the WHOLE log, not the query result — ranking inside the result would renumber every contact the moment a filter is applied, and QSO #1 would change identity as the operator typed. Held as one id-to-rank map, built from a single ordered id query and dropped with the other derived indexes when the log changes. A contact logged from the entry form is the newest, so it takes the next number without rereading the log: AddQSO deliberately avoids full invalidation because a contest run would pay for it once per QSO. A contact entered with an OLDER date belongs in the middle of the order, so there the map is dropped and rebuilt rather than mis-numbered.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hamlog/internal/qso"
|
||||
)
|
||||
|
||||
func at(s string) time.Time {
|
||||
t, err := time.Parse("2006-01-02 15:04", s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// The number is the contact's rank in the WHOLE log, not its position in what
|
||||
// the grid happens to be showing. Ranking inside the result would renumber
|
||||
// every contact the moment a filter is applied, and QSO #1 would change
|
||||
// identity as the operator typed.
|
||||
func TestQSONumberIsGlobalNotPerPage(t *testing.T) {
|
||||
a := &App{}
|
||||
// The log, oldest first: ids are deliberately NOT in date order, which is
|
||||
// exactly what an imported ADIF produces.
|
||||
a.qsoNumbers = map[int64]int{
|
||||
770: 1, // oldest, imported last so it has the highest id
|
||||
101: 2,
|
||||
102: 3,
|
||||
103: 4,
|
||||
}
|
||||
|
||||
// A filtered page holding only two of them, newest first as the grid asks.
|
||||
page := []qso.QSO{{ID: 103}, {ID: 770}}
|
||||
a.stampQSONumbers(page)
|
||||
|
||||
if page[0].Number != 4 {
|
||||
t.Errorf("id 103 numbered %d, want 4", page[0].Number)
|
||||
}
|
||||
if page[1].Number != 1 {
|
||||
t.Errorf("id 770 numbered %d, want 1 — the oldest contact, whatever its id", page[1].Number)
|
||||
}
|
||||
}
|
||||
|
||||
// A contact logged now is the newest, so it takes the next number without
|
||||
// rereading the log — a full scan per QSO would be felt in a contest run.
|
||||
func TestNewQSOTakesTheNextNumber(t *testing.T) {
|
||||
a := &App{}
|
||||
a.qsoNumbers = map[int64]int{1: 1, 2: 2, 3: 3}
|
||||
a.qsoNumMax = at("2026-08-13 10:00")
|
||||
|
||||
a.noteQSONumbered(9, at("2026-08-13 11:00"))
|
||||
if got := a.qsoNumbers[9]; got != 4 {
|
||||
t.Errorf("new QSO numbered %d, want 4", got)
|
||||
}
|
||||
if !a.qsoNumMax.Equal(at("2026-08-13 11:00")) {
|
||||
t.Error("the newest date was not carried forward")
|
||||
}
|
||||
}
|
||||
|
||||
// A contact entered with an OLDER date belongs in the middle of the order.
|
||||
// Appending it would number it last, which is wrong — so the map is dropped and
|
||||
// rebuilt correctly instead.
|
||||
func TestBackdatedQSOForcesARebuild(t *testing.T) {
|
||||
a := &App{}
|
||||
a.qsoNumbers = map[int64]int{1: 1, 2: 2, 3: 3}
|
||||
a.qsoNumMax = at("2026-08-13 10:00")
|
||||
|
||||
a.noteQSONumbered(9, at("2020-01-01 09:00"))
|
||||
if a.qsoNumbers != nil {
|
||||
t.Errorf("a back-dated QSO was appended as the newest: %v", a.qsoNumbers)
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing built yet: the lazy build will see the new contact anyway, so this
|
||||
// must not create a one-entry map that then numbers the whole log wrongly.
|
||||
func TestNoteBeforeAnyBuildDoesNothing(t *testing.T) {
|
||||
a := &App{}
|
||||
a.noteQSONumbered(9, at("2026-08-13 11:00"))
|
||||
if a.qsoNumbers != nil {
|
||||
t.Errorf("built a map from a single contact: %v", a.qsoNumbers)
|
||||
}
|
||||
}
|
||||
|
||||
// With no index available the column is simply empty — never wrong.
|
||||
func TestStampWithoutIndexLeavesZero(t *testing.T) {
|
||||
a := &App{} // no qso repo, so the index cannot be built
|
||||
page := []qso.QSO{{ID: 1}, {ID: 2}}
|
||||
a.stampQSONumbers(page)
|
||||
for _, q := range page {
|
||||
if q.Number != 0 {
|
||||
t.Errorf("invented a number without an index: %d", q.Number)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user