This commit is contained in:
2026-06-06 14:16:30 +02:00
parent f91f9ff3b8
commit 17f7a00bd7
19 changed files with 1278 additions and 91 deletions
+38 -5
View File
@@ -92,7 +92,7 @@ func (e *Exporter) writeDoc(ctx context.Context, w io.Writer, iter iterator) (in
ver := strings.TrimSpace(e.AppVersion)
now := time.Now().UTC().Format("20060102 150405")
fmt.Fprintf(bw, "# ADIF export by %s %s — %s UTC\n", app, ver, now)
fmt.Fprintf(bw, "<ADIF_VER:5>3.1.0 <PROGRAMID:%d>%s", len(app), app)
fmt.Fprintf(bw, "<ADIF_VER:%d>%s <PROGRAMID:%d>%s", len(adifVersion), adifVersion, len(app), app)
if ver != "" {
fmt.Fprintf(bw, " <PROGRAMVERSION:%d>%s", len(ver), ver)
}
@@ -248,13 +248,46 @@ func writeRecord(bw *bufio.Writer, q qso.QSO, includeApp bool) {
writeField(bw, "COMMENT", q.Comment)
writeField(bw, "NOTES", q.Notes)
// --- ADIF 3.1.7 additional promoted fields ---
writeField(bw, "SIG", q.SIG)
writeField(bw, "SIG_INFO", q.SIGInfo)
writeField(bw, "MY_SIG", q.MySIG)
writeField(bw, "MY_SIG_INFO", q.MySIGInfo)
writeField(bw, "WWFF_REF", q.WWFFRef)
writeField(bw, "MY_WWFF_REF", q.MyWWFFRef)
writeFloatPtr(bw, "DISTANCE", q.Distance, 1)
writeFloatPtr(bw, "RX_PWR", q.RXPower, 1)
writeFloatPtr(bw, "A_INDEX", q.AIndex, 0)
writeFloatPtr(bw, "K_INDEX", q.KIndex, 0)
writeFloatPtr(bw, "SFI", q.SFI, 0)
writeField(bw, "SKCC", q.SKCC)
writeField(bw, "FISTS", q.FISTS)
writeField(bw, "TEN_TEN", q.TenTen)
writeField(bw, "CONTACTED_OP", q.ContactedOp)
writeField(bw, "EQ_CALL", q.EqCall)
writeField(bw, "PFX", q.PFX)
writeField(bw, "MY_NAME", q.MyName)
writeField(bw, "CLASS", q.Class)
writeField(bw, "DARC_DOK", q.DarcDOK)
writeField(bw, "MY_DARC_DOK", q.MyDarcDOK)
writeField(bw, "REGION", q.Region)
writeField(bw, "SILENT_KEY", q.SilentKey)
writeField(bw, "SWL", q.SWL)
writeField(bw, "QSO_COMPLETE", q.QSOComplete)
writeField(bw, "QSO_RANDOM", q.QSORandom)
writeField(bw, "CREDIT_GRANTED", q.CreditGranted)
writeField(bw, "CREDIT_SUBMITTED", q.CreditSubmitted)
writeField(bw, "MY_ARRL_SECT", q.MyARRLSect)
writeField(bw, "MY_VUCC_GRIDS", q.MyVUCCGrids)
// --- Extras (unpromoted ADIF fields preserved verbatim) ---
// In standard mode we drop application-specific tags (APP_*) so the file
// stays portable to other loggers; in full mode they're kept for a
// lossless OpsLog round-trip.
// Standard mode emits ONLY valid ADIF-spec fields, so it drops APP_*
// application-specific tags AND any non-standard / vendor tag — keeping
// the file strictly portable to other loggers. Full mode keeps every
// extra for a lossless OpsLog round-trip.
for k, v := range q.Extras {
tag := strings.ToUpper(k)
if !includeApp && strings.HasPrefix(tag, "APP_") {
if !includeApp && (strings.HasPrefix(tag, "APP_") || !IsStandardField(tag)) {
continue
}
writeField(bw, tag, v)
+272
View File
@@ -0,0 +1,272 @@
package adif
import "strings"
// This file embeds the complete ADIF 3.1.7 QSO-field dictionary. It is the
// single source of truth for:
// - the generic "ADIF fields" editor in the UI (any field becomes editable),
// - the "standard ADIF" export mode (only spec fields are emitted),
// - import diagnostics (a tag absent here is non-standard / vendor-specific).
//
// Promoted fields (those with dedicated QSO columns) are flagged Promoted=true
// so the UI can hide them from the generic editor — they already have proper
// inputs in the main tabs.
// FieldKind is a coarse input type used by the generic editor to pick a widget.
type FieldKind string
const (
KindText FieldKind = "text" // String / IntlString / MultilineString
KindNumber FieldKind = "number" // Number / PositiveInteger
KindDate FieldKind = "date" // ADIF Date (YYYYMMDD)
KindTime FieldKind = "time" // ADIF Time (HHMMSS / HHMM)
KindBool FieldKind = "boolean" // Boolean (Y/N)
KindEnum FieldKind = "enum" // Enumeration
KindLoc FieldKind = "location" // Location (e.g. "N048 09.000")
)
// FieldDef describes one ADIF QSO field.
type FieldDef struct {
Name string `json:"name"` // canonical uppercase ADIF tag
Kind FieldKind `json:"kind"` // editor widget hint
Category string `json:"category"` // grouping for the UI
Promoted bool `json:"promoted"` // has a dedicated QSO column
Deprecated bool `json:"deprecated"` // import-only per the spec
Intl bool `json:"intl"` // *_INTL UTF-8 variant
}
// adifVersion is the ADIF spec version OpsLog targets for import/export.
const adifVersion = "3.1.7"
// ADIFVersion returns the ADIF spec version OpsLog conforms to.
func ADIFVersion() string { return adifVersion }
// Fields is the full ADIF 3.1.7 QSO-field set. Order is alphabetical within
// each category; categories order is roughly "most-used first" for the UI.
var Fields = []FieldDef{
// ── Core / contact ──────────────────────────────────────────────
{Name: "CALL", Kind: KindText, Category: "Core", Promoted: true},
{Name: "QSO_DATE", Kind: KindDate, Category: "Core", Promoted: true},
{Name: "TIME_ON", Kind: KindTime, Category: "Core", Promoted: true},
{Name: "QSO_DATE_OFF", Kind: KindDate, Category: "Core", Promoted: true},
{Name: "TIME_OFF", Kind: KindTime, Category: "Core", Promoted: true},
{Name: "BAND", Kind: KindEnum, Category: "Core", Promoted: true},
{Name: "BAND_RX", Kind: KindEnum, Category: "Core", Promoted: true},
{Name: "MODE", Kind: KindEnum, Category: "Core", Promoted: true},
{Name: "SUBMODE", Kind: KindEnum, Category: "Core", Promoted: true},
{Name: "FREQ", Kind: KindNumber, Category: "Core", Promoted: true},
{Name: "FREQ_RX", Kind: KindNumber, Category: "Core", Promoted: true},
{Name: "RST_SENT", Kind: KindText, Category: "Core", Promoted: true},
{Name: "RST_RCVD", Kind: KindText, Category: "Core", Promoted: true},
{Name: "QSO_COMPLETE", Kind: KindEnum, Category: "Core", Promoted: true},
{Name: "QSO_RANDOM", Kind: KindBool, Category: "Core", Promoted: true},
{Name: "SWL", Kind: KindBool, Category: "Core", Promoted: true},
// ── Contacted station ───────────────────────────────────────────
{Name: "NAME", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "NAME_INTL", Kind: KindText, Category: "Contacted", Intl: true},
{Name: "QTH", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "QTH_INTL", Kind: KindText, Category: "Contacted", Intl: true},
{Name: "ADDRESS", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "ADDRESS_INTL", Kind: KindText, Category: "Contacted", Intl: true},
{Name: "EMAIL", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "WEB", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "GRIDSQUARE", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "GRIDSQUARE_EXT", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "VUCC_GRIDS", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "COUNTRY", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "COUNTRY_INTL", Kind: KindText, Category: "Contacted", Intl: true},
{Name: "STATE", Kind: KindEnum, Category: "Contacted", Promoted: true},
{Name: "CNTY", Kind: KindEnum, Category: "Contacted", Promoted: true},
{Name: "CNTY_ALT", Kind: KindEnum, Category: "Contacted"},
{Name: "DXCC", Kind: KindEnum, Category: "Contacted", Promoted: true},
{Name: "CONT", Kind: KindEnum, Category: "Contacted", Promoted: true},
{Name: "CQZ", Kind: KindNumber, Category: "Contacted", Promoted: true},
{Name: "ITUZ", Kind: KindNumber, Category: "Contacted", Promoted: true},
{Name: "IOTA", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "IOTA_ISLAND_ID", Kind: KindText, Category: "Contacted"},
{Name: "REGION", Kind: KindEnum, Category: "Contacted", Promoted: true},
{Name: "PFX", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "AGE", Kind: KindNumber, Category: "Contacted", Promoted: true},
{Name: "LAT", Kind: KindLoc, Category: "Contacted", Promoted: true},
{Name: "LON", Kind: KindLoc, Category: "Contacted", Promoted: true},
{Name: "ALTITUDE", Kind: KindNumber, Category: "Contacted"},
{Name: "DISTANCE", Kind: KindNumber, Category: "Contacted", Promoted: true},
{Name: "RIG", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "RIG_INTL", Kind: KindText, Category: "Contacted", Intl: true},
{Name: "ANT", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "CONTACTED_OP", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "EQ_CALL", Kind: KindText, Category: "Contacted", Promoted: true},
{Name: "GUEST_OP", Kind: KindText, Category: "Contacted", Deprecated: true},
{Name: "OWNER_CALLSIGN", Kind: KindText, Category: "Contacted"},
{Name: "SILENT_KEY", Kind: KindBool, Category: "Contacted", Promoted: true},
{Name: "USACA_COUNTIES", Kind: KindText, Category: "Contacted"},
// ── Special activity (POTA/SOTA/WWFF/SIG) ───────────────────────
{Name: "SIG", Kind: KindText, Category: "Activity", Promoted: true},
{Name: "SIG_INFO", Kind: KindText, Category: "Activity", Promoted: true},
{Name: "SIG_INTL", Kind: KindText, Category: "Activity", Intl: true},
{Name: "SIG_INFO_INTL", Kind: KindText, Category: "Activity", Intl: true},
{Name: "POTA_REF", Kind: KindText, Category: "Activity", Promoted: true},
{Name: "SOTA_REF", Kind: KindText, Category: "Activity", Promoted: true},
{Name: "WWFF_REF", Kind: KindText, Category: "Activity", Promoted: true},
// ── Power / propagation / space wx ──────────────────────────────
{Name: "TX_PWR", Kind: KindNumber, Category: "Propagation", Promoted: true},
{Name: "RX_PWR", Kind: KindNumber, Category: "Propagation", Promoted: true},
{Name: "A_INDEX", Kind: KindNumber, Category: "Propagation", Promoted: true},
{Name: "K_INDEX", Kind: KindNumber, Category: "Propagation", Promoted: true},
{Name: "SFI", Kind: KindNumber, Category: "Propagation", Promoted: true},
{Name: "PROP_MODE", Kind: KindEnum, Category: "Propagation", Promoted: true},
{Name: "SAT_NAME", Kind: KindText, Category: "Propagation", Promoted: true},
{Name: "SAT_MODE", Kind: KindText, Category: "Propagation", Promoted: true},
{Name: "ANT_AZ", Kind: KindNumber, Category: "Propagation", Promoted: true},
{Name: "ANT_EL", Kind: KindNumber, Category: "Propagation", Promoted: true},
{Name: "ANT_PATH", Kind: KindEnum, Category: "Propagation", Promoted: true},
{Name: "FORCE_INIT", Kind: KindBool, Category: "Propagation"},
{Name: "MAX_BURSTS", Kind: KindNumber, Category: "Propagation"},
{Name: "MS_SHOWER", Kind: KindText, Category: "Propagation"},
{Name: "NR_BURSTS", Kind: KindNumber, Category: "Propagation"},
{Name: "NR_PINGS", Kind: KindNumber, Category: "Propagation"},
// ── QSL / confirmations ─────────────────────────────────────────
{Name: "QSL_SENT", Kind: KindEnum, Category: "QSL", Promoted: true},
{Name: "QSL_RCVD", Kind: KindEnum, Category: "QSL", Promoted: true},
{Name: "QSLSDATE", Kind: KindDate, Category: "QSL", Promoted: true},
{Name: "QSLRDATE", Kind: KindDate, Category: "QSL", Promoted: true},
{Name: "QSL_VIA", Kind: KindText, Category: "QSL", Promoted: true},
{Name: "QSL_SENT_VIA", Kind: KindEnum, Category: "QSL"},
{Name: "QSL_RCVD_VIA", Kind: KindEnum, Category: "QSL"},
{Name: "QSLMSG", Kind: KindText, Category: "QSL", Promoted: true},
{Name: "QSLMSG_INTL", Kind: KindText, Category: "QSL", Intl: true},
{Name: "QSLMSG_RCVD", Kind: KindText, Category: "QSL", Promoted: true},
{Name: "LOTW_QSL_SENT", Kind: KindEnum, Category: "QSL", Promoted: true},
{Name: "LOTW_QSL_RCVD", Kind: KindEnum, Category: "QSL", Promoted: true},
{Name: "LOTW_QSLSDATE", Kind: KindDate, Category: "QSL", Promoted: true},
{Name: "LOTW_QSLRDATE", Kind: KindDate, Category: "QSL", Promoted: true},
{Name: "EQSL_QSL_SENT", Kind: KindEnum, Category: "QSL", Promoted: true},
{Name: "EQSL_QSL_RCVD", Kind: KindEnum, Category: "QSL", Promoted: true},
{Name: "EQSL_QSLSDATE", Kind: KindDate, Category: "QSL", Promoted: true},
{Name: "EQSL_QSLRDATE", Kind: KindDate, Category: "QSL", Promoted: true},
{Name: "EQSL_AG", Kind: KindBool, Category: "QSL"},
{Name: "DCL_QSL_SENT", Kind: KindEnum, Category: "QSL"},
{Name: "DCL_QSL_RCVD", Kind: KindEnum, Category: "QSL"},
{Name: "DCL_QSLSDATE", Kind: KindDate, Category: "QSL"},
{Name: "DCL_QSLRDATE", Kind: KindDate, Category: "QSL"},
{Name: "CLUBLOG_QSO_UPLOAD_DATE", Kind: KindDate, Category: "QSL", Promoted: true},
{Name: "CLUBLOG_QSO_UPLOAD_STATUS", Kind: KindEnum, Category: "QSL", Promoted: true},
{Name: "HRDLOG_QSO_UPLOAD_DATE", Kind: KindDate, Category: "QSL", Promoted: true},
{Name: "HRDLOG_QSO_UPLOAD_STATUS", Kind: KindEnum, Category: "QSL", Promoted: true},
{Name: "QRZCOM_QSO_UPLOAD_DATE", Kind: KindDate, Category: "QSL", Promoted: true},
{Name: "QRZCOM_QSO_UPLOAD_STATUS", Kind: KindEnum, Category: "QSL", Promoted: true},
{Name: "QRZCOM_QSO_DOWNLOAD_DATE", Kind: KindDate, Category: "QSL", Promoted: true},
{Name: "QRZCOM_QSO_DOWNLOAD_STATUS", Kind: KindEnum, Category: "QSL", Promoted: true},
{Name: "HAMLOGEU_QSO_UPLOAD_DATE", Kind: KindDate, Category: "QSL"},
{Name: "HAMLOGEU_QSO_UPLOAD_STATUS", Kind: KindEnum, Category: "QSL"},
{Name: "HAMQTH_QSO_UPLOAD_DATE", Kind: KindDate, Category: "QSL"},
{Name: "HAMQTH_QSO_UPLOAD_STATUS", Kind: KindEnum, Category: "QSL"},
// ── Awards / credits ────────────────────────────────────────────
{Name: "CREDIT_SUBMITTED", Kind: KindText, Category: "Awards", Promoted: true},
{Name: "CREDIT_GRANTED", Kind: KindText, Category: "Awards", Promoted: true},
{Name: "AWARD_SUBMITTED", Kind: KindText, Category: "Awards"},
{Name: "AWARD_GRANTED", Kind: KindText, Category: "Awards"},
// ── Contest ─────────────────────────────────────────────────────
{Name: "CONTEST_ID", Kind: KindText, Category: "Contest", Promoted: true},
{Name: "SRX", Kind: KindNumber, Category: "Contest", Promoted: true},
{Name: "STX", Kind: KindNumber, Category: "Contest", Promoted: true},
{Name: "SRX_STRING", Kind: KindText, Category: "Contest", Promoted: true},
{Name: "STX_STRING", Kind: KindText, Category: "Contest", Promoted: true},
{Name: "CHECK", Kind: KindText, Category: "Contest", Promoted: true},
{Name: "CLASS", Kind: KindText, Category: "Contest", Promoted: true},
{Name: "PRECEDENCE", Kind: KindText, Category: "Contest", Promoted: true},
{Name: "ARRL_SECT", Kind: KindEnum, Category: "Contest", Promoted: true},
// ── Club memberships ────────────────────────────────────────────
{Name: "SKCC", Kind: KindText, Category: "Clubs", Promoted: true},
{Name: "FISTS", Kind: KindNumber, Category: "Clubs", Promoted: true},
{Name: "FISTS_CC", Kind: KindNumber, Category: "Clubs"},
{Name: "TEN_TEN", Kind: KindNumber, Category: "Clubs", Promoted: true},
{Name: "UKSMG", Kind: KindNumber, Category: "Clubs"},
{Name: "DARC_DOK", Kind: KindText, Category: "Clubs", Promoted: true},
// ── Morse key (3.1.5+) ──────────────────────────────────────────
{Name: "MORSE_KEY_TYPE", Kind: KindEnum, Category: "Morse key"},
{Name: "MORSE_KEY_INFO", Kind: KindText, Category: "Morse key"},
// ── Misc / crypto ───────────────────────────────────────────────
{Name: "COMMENT", Kind: KindText, Category: "Misc", Promoted: true},
{Name: "COMMENT_INTL", Kind: KindText, Category: "Misc", Intl: true},
{Name: "NOTES", Kind: KindText, Category: "Misc", Promoted: true},
{Name: "NOTES_INTL", Kind: KindText, Category: "Misc", Intl: true},
{Name: "PUBLIC_KEY", Kind: KindText, Category: "Misc"},
{Name: "VE_PROV", Kind: KindText, Category: "Misc", Deprecated: true},
// ── My station / operator ───────────────────────────────────────
{Name: "STATION_CALLSIGN", Kind: KindText, Category: "My station", Promoted: true},
{Name: "OPERATOR", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_NAME", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_NAME_INTL", Kind: KindText, Category: "My station", Intl: true},
{Name: "MY_GRIDSQUARE", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_GRIDSQUARE_EXT", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_VUCC_GRIDS", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_COUNTRY", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_COUNTRY_INTL", Kind: KindText, Category: "My station", Intl: true},
{Name: "MY_STATE", Kind: KindEnum, Category: "My station", Promoted: true},
{Name: "MY_CNTY", Kind: KindEnum, Category: "My station", Promoted: true},
{Name: "MY_CNTY_ALT", Kind: KindEnum, Category: "My station"},
{Name: "MY_DXCC", Kind: KindEnum, Category: "My station", Promoted: true},
{Name: "MY_CQ_ZONE", Kind: KindNumber, Category: "My station", Promoted: true},
{Name: "MY_ITU_ZONE", Kind: KindNumber, Category: "My station", Promoted: true},
{Name: "MY_IOTA", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_IOTA_ISLAND_ID", Kind: KindText, Category: "My station"},
{Name: "MY_SOTA_REF", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_POTA_REF", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_WWFF_REF", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_SIG", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_SIG_INFO", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_SIG_INTL", Kind: KindText, Category: "My station", Intl: true},
{Name: "MY_SIG_INFO_INTL", Kind: KindText, Category: "My station", Intl: true},
{Name: "MY_LAT", Kind: KindLoc, Category: "My station", Promoted: true},
{Name: "MY_LON", Kind: KindLoc, Category: "My station", Promoted: true},
{Name: "MY_ALTITUDE", Kind: KindNumber, Category: "My station"},
{Name: "MY_STREET", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_STREET_INTL", Kind: KindText, Category: "My station", Intl: true},
{Name: "MY_CITY", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_CITY_INTL", Kind: KindText, Category: "My station", Intl: true},
{Name: "MY_POSTAL_CODE", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_POSTAL_CODE_INTL", Kind: KindText, Category: "My station", Intl: true},
{Name: "MY_RIG", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_RIG_INTL", Kind: KindText, Category: "My station", Intl: true},
{Name: "MY_ANTENNA", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_ANTENNA_INTL", Kind: KindText, Category: "My station", Intl: true},
{Name: "MY_ARRL_SECT", Kind: KindEnum, Category: "My station", Promoted: true},
{Name: "MY_USACA_COUNTIES", Kind: KindText, Category: "My station"},
{Name: "MY_DARC_DOK", Kind: KindText, Category: "My station", Promoted: true},
{Name: "MY_FISTS", Kind: KindNumber, Category: "My station"},
{Name: "MY_MORSE_KEY_TYPE", Kind: KindEnum, Category: "My station"},
{Name: "MY_MORSE_KEY_INFO", Kind: KindText, Category: "My station"},
}
// fieldIndex maps an uppercase ADIF tag to its definition for O(1) lookup.
var fieldIndex = func() map[string]FieldDef {
m := make(map[string]FieldDef, len(Fields))
for _, f := range Fields {
m[f.Name] = f
}
return m
}()
// IsStandardField reports whether tag (any case) is a defined ADIF 3.1.7
// field. APP_* and USERDEF tags are non-standard and return false.
func IsStandardField(tag string) bool {
_, ok := fieldIndex[strings.ToUpper(strings.TrimSpace(tag))]
return ok
}
// LookupField returns the definition for a tag (any case), ok=false if unknown.
func LookupField(tag string) (FieldDef, bool) {
f, ok := fieldIndex[strings.ToUpper(strings.TrimSpace(tag))]
return f, ok
}
+48
View File
@@ -284,6 +284,12 @@ var adifPromoted = stringSet(
"my_street", "my_city", "my_postal_code", "my_rig", "my_antenna",
// Misc
"tx_pwr", "comment", "notes",
// ADIF 3.1.7 additional promoted fields
"sig", "sig_info", "my_sig", "my_sig_info", "wwff_ref", "my_wwff_ref",
"distance", "rx_pwr", "a_index", "k_index", "sfi",
"skcc", "fists", "ten_ten", "contacted_op", "eq_call", "pfx", "my_name", "class",
"darc_dok", "my_darc_dok", "region", "silent_key", "swl", "qso_complete", "qso_random",
"credit_granted", "credit_submitted", "my_arrl_sect", "my_vucc_grids",
)
func stringSet(items ...string) map[string]struct{} {
@@ -482,6 +488,48 @@ func recordToQSO(rec Record) (qso.QSO, bool) {
q.Comment = rec["comment"]
q.Notes = rec["notes"]
// ADIF 3.1.7 additional promoted fields
q.SIG = rec["sig"]
q.SIGInfo = rec["sig_info"]
q.MySIG = rec["my_sig"]
q.MySIGInfo = rec["my_sig_info"]
q.WWFFRef = strings.ToUpper(rec["wwff_ref"])
q.MyWWFFRef = strings.ToUpper(rec["my_wwff_ref"])
if v, ok := parseFloat(rec["distance"]); ok {
q.Distance = &v
}
if v, ok := parseFloat(rec["rx_pwr"]); ok {
q.RXPower = &v
}
if v, ok := parseFloat(rec["a_index"]); ok {
q.AIndex = &v
}
if v, ok := parseFloat(rec["k_index"]); ok {
q.KIndex = &v
}
if v, ok := parseFloat(rec["sfi"]); ok {
q.SFI = &v
}
q.SKCC = rec["skcc"]
q.FISTS = rec["fists"]
q.TenTen = rec["ten_ten"]
q.ContactedOp = strings.ToUpper(rec["contacted_op"])
q.EqCall = strings.ToUpper(rec["eq_call"])
q.PFX = strings.ToUpper(rec["pfx"])
q.MyName = rec["my_name"]
q.Class = rec["class"]
q.DarcDOK = rec["darc_dok"]
q.MyDarcDOK = rec["my_darc_dok"]
q.Region = rec["region"]
q.SilentKey = strings.ToUpper(rec["silent_key"])
q.SWL = strings.ToUpper(rec["swl"])
q.QSOComplete = rec["qso_complete"]
q.QSORandom = strings.ToUpper(rec["qso_random"])
q.CreditGranted = rec["credit_granted"]
q.CreditSubmitted = rec["credit_submitted"]
q.MyARRLSect = strings.ToUpper(rec["my_arrl_sect"])
q.MyVUCCGrids = strings.ToUpper(rec["my_vucc_grids"])
// Everything else lands in extras (uppercased ADIF names).
var extras map[string]string
for k, v := range rec {
+121
View File
@@ -0,0 +1,121 @@
package adif
import (
"bufio"
"bytes"
"strings"
"testing"
"time"
"hamlog/internal/qso"
)
// TestPromotedFieldsRoundTrip writes a QSO carrying the ADIF 3.1.7 promoted
// fields, parses it back, and checks they survive — guarding the export
// writeRecord ↔ import recordToQSO field-name mapping against typos.
func TestPromotedFieldsRoundTrip(t *testing.T) {
dist := 1234.5
rxp := 5.0
a := 12.0
in := qso.QSO{
Callsign: "EA8ABC", Band: "20m", Mode: "SSB",
QSODate: time.Date(2026, 6, 6, 12, 0, 0, 0, time.UTC),
SIG: "POTA", SIGInfo: "US-0001", MySIG: "WWFF", MySIGInfo: "ONFF-0001",
WWFFRef: "ONFF-0001", MyWWFFRef: "F-FFF-0001",
Distance: &dist, RXPower: &rxp, AIndex: &a,
SKCC: "12345S", FISTS: "999", TenTen: "55555",
ContactedOp: "EA8XYZ", EqCall: "EA8OLD", PFX: "EA8", MyName: "Greg",
Class: "1A", DarcDOK: "A01", MyDarcDOK: "B02", Region: "IV",
SilentKey: "N", SWL: "N", QSOComplete: "Y", QSORandom: "Y",
CreditGranted: "DXCC", CreditSubmitted: "WAS",
MyARRLSect: "EMA", MyVUCCGrids: "FN20,FN21",
}
var buf bytes.Buffer
bw := bufio.NewWriter(&buf)
bw.WriteString("<EOH>\n")
writeRecord(bw, in, true)
bw.Flush()
var rec Record
if err := Parse(strings.NewReader(buf.String()), func(r Record) error { rec = r; return nil }); err != nil {
t.Fatalf("parse: %v", err)
}
out, ok := recordToQSO(rec)
if !ok {
t.Fatal("recordToQSO returned !ok")
}
checks := map[string]struct{ got, want string }{
"SIG": {out.SIG, in.SIG},
"SIG_INFO": {out.SIGInfo, in.SIGInfo},
"MY_SIG": {out.MySIG, in.MySIG},
"MY_SIG_INFO": {out.MySIGInfo, in.MySIGInfo},
"WWFF_REF": {out.WWFFRef, in.WWFFRef},
"MY_WWFF_REF": {out.MyWWFFRef, in.MyWWFFRef},
"SKCC": {out.SKCC, in.SKCC},
"FISTS": {out.FISTS, in.FISTS},
"TEN_TEN": {out.TenTen, in.TenTen},
"CONTACTED_OP": {out.ContactedOp, in.ContactedOp},
"EQ_CALL": {out.EqCall, in.EqCall},
"PFX": {out.PFX, in.PFX},
"MY_NAME": {out.MyName, in.MyName},
"CLASS": {out.Class, in.Class},
"DARC_DOK": {out.DarcDOK, in.DarcDOK},
"MY_DARC_DOK": {out.MyDarcDOK, in.MyDarcDOK},
"REGION": {out.Region, in.Region},
"SILENT_KEY": {out.SilentKey, in.SilentKey},
"SWL": {out.SWL, in.SWL},
"QSO_COMPLETE": {out.QSOComplete, in.QSOComplete},
"QSO_RANDOM": {out.QSORandom, in.QSORandom},
"CREDIT_GRANTED": {out.CreditGranted, in.CreditGranted},
"CREDIT_SUBMITTED": {out.CreditSubmitted, in.CreditSubmitted},
"MY_ARRL_SECT": {out.MyARRLSect, in.MyARRLSect},
"MY_VUCC_GRIDS": {out.MyVUCCGrids, in.MyVUCCGrids},
}
for tag, c := range checks {
if c.got != c.want {
t.Errorf("%s round-trip = %q, want %q", tag, c.got, c.want)
}
}
if out.Distance == nil || *out.Distance != dist {
t.Errorf("DISTANCE round-trip = %v, want %v", out.Distance, dist)
}
if out.RXPower == nil || *out.RXPower != rxp {
t.Errorf("RX_PWR round-trip = %v, want %v", out.RXPower, rxp)
}
if out.AIndex == nil || *out.AIndex != a {
t.Errorf("A_INDEX round-trip = %v, want %v", out.AIndex, a)
}
}
// TestStandardExportDropsNonStandard verifies that standard mode strips
// vendor/APP tags while full mode keeps them.
func TestStandardExportDropsNonStandard(t *testing.T) {
q := qso.QSO{
Callsign: "F4BPO", Band: "20m", Mode: "CW",
Extras: map[string]string{
"APP_LOG4OM_FOO": "x",
"DARC_DOK": "A01", // standard → kept in both
"MY_VENDOR_TAG": "y", // non-standard → dropped in standard mode
},
}
standard := renderRecord(q, false)
if strings.Contains(standard, "APP_LOG4OM_FOO") || strings.Contains(standard, "MY_VENDOR_TAG") {
t.Errorf("standard export should drop non-standard tags:\n%s", standard)
}
full := renderRecord(q, true)
if !strings.Contains(full, "APP_LOG4OM_FOO") || !strings.Contains(full, "MY_VENDOR_TAG") {
t.Errorf("full export should keep all extras:\n%s", full)
}
}
func renderRecord(q qso.QSO, includeApp bool) string {
var buf bytes.Buffer
bw := bufio.NewWriter(&buf)
writeRecord(bw, q, includeApp)
bw.Flush()
return buf.String()
}