adif

package module
v5.0.0-beta.24 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2025 License: BSD-3-Clause Imports: 13 Imported by: 0

README

⚡ High Performance ADI Parser for Go

This library provides high-performance processing of ADIF (Amateur Data Interchange Format) ADI files used for ham radio logs. It's idiomatic, developer-friendly API seamlessly integrates with your codebase and the go standard library.

Tests Go Report Card Go Reference Go Version License

✨ Features

  • 🔬 Tested: 100% test coverage!
  • 🔧 Developer Friendly: Clean, idiomatic, mock friendly interfaces
  • 🚀 Blazing Fast: 2.4x-20x faster than other libraries
  • 💡 Memory Efficient: Uses 2.2x less memory and makes 3.8 fewer allocations than the nearest competitor.

🚀 Quick Start

go get github.com/farmergreg/adif/v5
  1. ADI Processing Example
  2. ADX XML Processing: Not implemented. PR(s) welcome!
  3. JSON Processing Example: Experimental. Not optimized; not part of the ADIF Specification.

Benchmarks

Please see the Go ADIF Parser Benchmarks project for benchmarks.

TLDR, this library processes ADI data 3x faster than the go standard library can process the same data in json format. This library is 2.4x faster than the nearest ADI parser.

🔧 Technical Deep Dive (ADI Parser)

The ADI parser in this library achieves high performance through the following optimizations:

Performance Optimizations
  • Leverages stdlib I/O operations with SSE/SIMD acceleration depending upon your CPU architecture
  • Smart buffer pre-allocation based on discovered record sizes
  • Optimized base-10 integer parsing for ADIF field lengths
Memory Management
  • Zero-copy techniques minimize memory operations
  • String interning of repeated field names greatly reduces copying, allocations, and memory use
  • Minimal temporary allocations during field parsing
  • Dynamic buffer sizing based on learned field counts
  • Buffer pooling

If you found this library useful, you may also be interested in the following projects:

📝 License

This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.

Documentation

Overview

Package adif implements a high performance ADIF library for Go. It provides types, structs and methods for managing ADIF Records. Idiomatic interfaces for reading and writing ADI formatted data make integration with other Go libraries fast and easy.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrAdiReaderMalformedADI is returned when the ADI formatted data does not conform to the ADIF specification.
	ErrAdiReaderMalformedADI = errors.New("adi reader: data is malformed")

	// ErrAdiReaderNilReader is returned when the reader passed to NewADIDocumentReader is nil.
	ErrAdiWriterNilWriter = errors.New("adi writer: nil writer")

	// ErrHeaderAlreadyWritten is returned when attempting to write more than one header record.
	ErrHeaderAlreadyWritten = errors.New("header record already written")
)

Functions

This section is empty.

Types

type DocumentReader

type DocumentReader interface {
	// Next reads and returns the next Record.
	// It returns io.EOF when no more records are available.
	// isHeader indicates if the record is a header record.
	Next() (record Record, isHeader bool, err error)
}

DocumentReader reads Amateur Data Interchange Format (ADIF) records sequentially.

func NewADIDocumentReader

func NewADIDocumentReader(r io.Reader, skipHeader bool) DocumentReader

NewADIDocumentReader returns an ADIFReader that can parse ADIF *.adi formatted records. If skipHeader is true, Next() will not return the header record if it exists. This is a streaming parser that processes the input as it is read, using minimal memory.

Example
// Example ADI data
adiData := `
<ADIF_VERS:5>3.1.0
<PROGRAMID:4>Test
<EOH>
<CALL:5>K9CTS<QSO_DATE:8>20230101<TIME_ON:4>1200<BAND:3>20m<MODE:3>ssb<eor>
<CALL:5>W9PVA<QSO_DATE:8>20230102<TIME_ON:4>1300<BAND:3>40m<MODE:2>cw<eor>
`

reader := NewADIDocumentReader(strings.NewReader(adiData), true)
record, _, err := reader.Next()
for err == nil {
	fmt.Printf("Call: %s, Date: %s, Time: %s, Band: %s, Mode: %s\n",
		record.Get(adifield.CALL),
		record.Get(adifield.QSO_DATE),
		record.Get(adifield.TIME_ON),
		record.Get(adifield.BAND),
		record.Get(adifield.MODE))

	record, _, err = reader.Next()
}
if !errors.Is(err, io.EOF) {
	panic(err)
}
Output:

Call: K9CTS, Date: 20230101, Time: 1200, Band: 20m, Mode: ssb
Call: W9PVA, Date: 20230102, Time: 1300, Band: 40m, Mode: cw

func NewJSONDocumentReader

func NewJSONDocumentReader(r io.Reader, skipHeader bool) (DocumentReader, error)

NewJSONDocumentReader returns an ADIFDocumentReader that can parse ADIF records in ADIJ JSON format. If skipHeader is true, Next() will not return the header record if it exists.

Example
jsonExample := `{
  "HEADER": {
    "CREATED_TIMESTAMP": "20250907 212700",
    "PROGRAMID": "ExampleProgram",
    "PROGRAMVERSION": "1.0"
  },
  "RECORDS": [
    {
      "BAND": "20m",
      "CALL": "K9CTS",
      "MODE": "ssb",
      "QSO_DATE": "20250907",
      "TIME_ON": "2127"
    },
    {
      "BAND": "40m",
      "CALL": "W9PVA",
      "MODE": "cw",
      "QSO_DATE": "20250907",
      "TIME_ON": "2130"
    }
  ]
}`

// Create a reader from the ADIJ data
reader, err := NewJSONDocumentReader(strings.NewReader(jsonExample), false)
if err != nil {
	fmt.Printf("Error creating reader: %v\n", err)
	return
}

record, isHeader, err := reader.Next()
for err == nil {
	fmt.Printf("Is Header: %v\n", isHeader)
	if isHeader {
		fmt.Printf("created_timestamp: %s\n", record.Get(adifield.CREATED_TIMESTAMP))
	} else {
		fmt.Printf("call: %s, band: %s, mode: %s\n", record.Get(adifield.CALL), record.Get(adifield.BAND), record.Get(adifield.MODE))
	}
	fmt.Println()
	record, isHeader, err = reader.Next()
}
if !errors.Is(err, io.EOF) {
	panic(err)
}
Output:

Is Header: true
created_timestamp: 20250907 212700

Is Header: false
call: K9CTS, band: 20m, mode: ssb

Is Header: false
call: W9PVA, band: 40m, mode: cw

type DocumentWriter

type DocumentWriter interface {
	// WriteHeader writes the ADIF header record to the output.
	// It MUST be called before using WriteRecord.
	// If WriteHeader is called more than once, it returns an error.
	WriteHeader(record Record) error

	// WriteRecord writes ADIF record(s) to the output.
	// When writing a header record, it MUST be the first record written.
	WriteRecord(record Record) error

	// Flush writes buffered data to the underlying writer.
	// IMPORTANT: This MUST be called once the header and records have been written to ensure all data is properly written.
	Flush() error
}

DocumentWriter writes Amateur Data Interchange Format (ADIF) records sequentially.

func NewADIDocumentWriter

func NewADIDocumentWriter(w io.Writer) DocumentWriter

NewADIDocumentWriter returns an ADIFDocumentWriter that can write ADIF *.adi formatted records.

Example

ExampleNewADIDocumentWriter demonstrates how to write an ADI document using NewADIDocumentWriter.

var sb strings.Builder
writer := NewADIDocumentWriter(&sb)

hdr := NewRecord()
hdr.Set(adifield.CREATED_TIMESTAMP, "20250907 212700")
writer.WriteHeader(hdr)

qso := NewRecord()
qso.Set(adifield.CALL, "K9CTS")
qso.Set(adifield.BAND, band.BAND_20M.String())
qso.Set(adifield.MODE, mode.SSB.String())
qso.Set(adifield.New("APP_Example"), "Example")
writer.WriteRecord(qso)

if err := writer.Flush(); err != nil {
	panic(err)
}

fmt.Println(sb.String())
Output:

AM✠DG
K9CTS High Performance ADIF Processing Library
   https://github.com/farmergreg/adif

<CREATED_TIMESTAMP:15>20250907 212700<EOH>
<BAND:3>20M<MODE:3>SSB<CALL:5>K9CTS<APP_EXAMPLE:7>Example<EOR>

func NewADIDocumentWriterWithPreamble

func NewADIDocumentWriterWithPreamble(w io.Writer, adiPreamble string) DocumentWriter

NewADIDocumentWriterWithPreamble returns an ADIFDocumentWriter that can write ADIF *.adi formatted records with a custom preamble for header records.

func NewJSONDocumentWriter

func NewJSONDocumentWriter(w io.Writer, indent string) DocumentWriter

NewJSONDocumentWriter creates a new ADIFDocumentWriter that writes ADIJ JSON to the provided io.Writer. The indent parameter specifies the string to use for indentation (e.g. "\t" or " "). An empty string means no indentation. JSON is not an official ADIF document container format. It is, however, useful for interoperability with other systems.

Example

ExampleNewADIJWriter demonstrates how to write ADIJ JSON document using NewADIJWriter.

var sb strings.Builder
writer := NewJSONDocumentWriter(&sb, "  ")

hdr := NewRecord()
hdr.Set(adifield.CREATED_TIMESTAMP, "20250907 212700")
writer.WriteHeader(hdr)

qso := NewRecord()
qso.Set(adifield.CALL, "K9CTS")
qso.Set(adifield.BAND, band.BAND_20M.String())
qso.Set(adifield.MODE, mode.SSB.String())
writer.WriteRecord(qso)

if err := writer.Flush(); err != nil {
	panic(err)
}

fmt.Println(sb.String())
Output:

{
  "HEADER": {
    "CREATED_TIMESTAMP": "20250907 212700"
  },
  "RECORDS": [
    {
      "BAND": "20M",
      "CALL": "K9CTS",
      "MODE": "SSB"
    }
  ]
}

type Record

type Record interface {
	Get(field adifield.Field) string        // Get returns the value for the specified field, or an empty string if the field is not present.
	Set(field adifield.Field, value string) // Set sets the value for the specified field.

	Fields() func(func(adifield.Field, string) bool) // Fields returns an iterator that yields field-value pairs for all fields in the record.
	FieldCount() int                                 // FieldCount returns the number of fields in the record.
}

Record represents a single ADIF record. It represents both Header and QSO records.

func NewRecord

func NewRecord() Record

NewRecord creates a new Record with the default initial capacity.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL