ocrline

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 22, 2026 License: MIT Imports: 6 Imported by: 0

README

ocrline

CI coverage Go Reference Go Report Card

Marshal and unmarshal Go structs to and from fixed-width line formats using struct tags.

Built for Scandinavian payment file formats (Nets AvtaleGiro, OCR Giro, Bankgirot AutoGiro) but works with any 80-character (or custom width) fixed-position record format.

Install

go get github.com/karolusz/ocrline

Usage

Define structs with ocr tags specifying 0-based byte positions (Go slice convention):

type TransmissionStart struct {
    FormatCode      string `ocr:"0:2"`
    ServiceCode     string `ocr:"2:4"`
    TransactionType string `ocr:"4:6"`
    RecordType      int    `ocr:"6:8"`
    DataTransmitter string `ocr:"8:16"`
    TransmissionNo  string `ocr:"16:23"`
    DataRecipient   string `ocr:"23:31"`
}
Unmarshal
line := "NY000010555555551000081000080800000000000000000000000000000000000000000000000000"

var record TransmissionStart
if err := ocrline.Unmarshal(line, &record); err != nil {
    log.Fatal(err)
}

fmt.Println(record.FormatCode)      // "NY"
fmt.Println(record.DataTransmitter) // "55555555"
fmt.Println(record.RecordType)      // 10
Marshal
record := TransmissionStart{
    FormatCode:      "NY",
    ServiceCode:     "00",
    TransactionType: "00",
    RecordType:      10,
    DataTransmitter: "55555555",
    TransmissionNo:  "1000081",
    DataRecipient:   "00008080",
}

line, err := ocrline.Marshal(record)
// "NY000010555555551000081000080800000000000000000000000000000000000000000000000000"

Tag Syntax

ocr:"start:end"
ocr:"start:end,option,option,..."

Positions are 0-based, exclusive end (like Go slices). ocr:"0:2" reads line[0:2].

Options
Option Description
align-left Left-align the value in the field
align-right Right-align the value in the field
pad-zero Pad with '0' characters
pad-space Pad with ' ' characters
omitempty If zero-valued, fill with padding instead of the value
Type Defaults
Go Type Default Alignment Default Padding
string left space
int, int8..int64 right zero
uint, uint8..uint64 right zero
bool right zero
*T inherits from T inherits from T

Named types follow their underlying type: type Numeric string gets string defaults.

Struct Composition

Embedded structs are flattened, just like encoding/json:

type RecordBase struct {
    FormatCode  string `ocr:"0:2"`
    ServiceCode string `ocr:"2:4"`
    RecordType  int    `ocr:"6:8"`
}

type PaymentRecord struct {
    RecordBase
    PayerNumber string `ocr:"15:31"`
    Amount      int    `ocr:"31:43"`
}

Embedded pointer structs (*RecordBase) are also supported. On unmarshal, nil pointers are auto-allocated. On marshal, nil embedded pointers are skipped (gaps filled with default).

Gap Filling

Byte positions not covered by any field are filled with '0' by default. To fill specific gaps with a different character, implement the Filler interface:

func (r PaymentRecord) OCRFill() []ocrline.Fill {
    return []ocrline.Fill{
        {Start: 8, End: 15, Char: ' '},  // positions 8-14 filled with spaces
    }
}

Custom Types

Implement Marshaler and Unmarshaler for full control over field serialization:

type ServiceCode string

func (s ServiceCode) MarshalOCR() (string, error) {
    return string(s), nil
}

func (s *ServiceCode) UnmarshalOCR(data string) error {
    *s = ServiceCode(strings.TrimSpace(data))
    return nil
}

Line Width

Marshal outputs 80 characters by default. Use MarshalWidth for other widths:

line, err := ocrline.MarshalWidth(record, 120)  // 120-char line
line, err := ocrline.MarshalWidth(record, 0)    // no padding, exact width of rightmost field

Validation and Caching

Struct metadata is parsed, validated, and cached per type on first use (same pattern as encoding/json). Subsequent calls for the same struct type have zero reflection overhead for tag parsing.

Validated once per type:

  • Tag syntax - start and end must be integers, start >= 0, end > start
  • Overlapping fields - two fields covering the same positions are rejected with *OverlapError
  • Invalid tags - malformed ocr tags produce *TagError

Validated per call:

  • Out-of-range fields - on unmarshal, fields exceeding the line length produce *UnmarshalRangeError
  • Unexported fields are silently skipped (same as encoding/json)

API

func Marshal(v any) (string, error)
func MarshalWidth(v any, width int) (string, error)
func Unmarshal(line string, v any) error
Interfaces
type Marshaler interface {
    MarshalOCR() (string, error)
}

type Unmarshaler interface {
    UnmarshalOCR(data string) error
}

type Filler interface {
    OCRFill() []Fill
}

Supported Formats

The library is format-agnostic. It has been designed with these formats in mind:

Format Country Provider Line Width
AvtaleGiro Norway Nets 80
OCR Giro Norway Nets 80
AutoGiro Sweden Bankgirot 80

License

MIT

Documentation

Overview

Package ocrline provides Marshal and Unmarshal functions for fixed-width line-based file formats such as Nets AvtaleGiro, OCR Giro, and Bankgirot AutoGiro.

Struct fields are annotated with `ocr` tags that specify their position within a fixed-width line, along with optional alignment and padding directives.

Tag Syntax

ocr:"start:end[,option...]"

Where start and end are zero-based byte positions (like Go slice indices), and options can be:

  • align-left, align-right — field alignment (default depends on type)
  • pad-zero, pad-space — padding character (default depends on type)
  • omitempty — if the field is zero-valued, fill with padding instead

Fields without an `ocr` tag are skipped. Embedded structs are traversed recursively.

Gaps Between Fields

Byte positions not covered by any struct field are filled with '0' by default. To fill specific gaps with a different character (e.g. spaces), implement the Filler interface on the record struct:

func (r PaymentClaim) OCRFill() []ocrline.Fill {
    return []ocrline.Fill{
        {Start: 21, End: 32, Char: ' '},
    }
}

Type Defaults

The library uses the Go type of each field to determine default alignment and padding:

  • int, int8..int64, uint..uint64: right-aligned, zero-padded
  • string: left-aligned, space-padded
  • Types implementing Marshaler / Unmarshaler: delegated to the type

Custom Types

Types can implement Marshaler and Unmarshaler to control their own serialization, similar to encoding/json:

type ServiceCode string

func (s ServiceCode) MarshalOCR() (string, error) { return string(s), nil }
func (s *ServiceCode) UnmarshalOCR(data string) error { *s = ServiceCode(data); return nil }

Validation

Struct tag metadata is parsed, validated, and cached on first use of a type (like encoding/json). Subsequent calls for the same struct type incur no reflection overhead. The following are validated once per type:

  • Tag syntax: start and end must be valid integers, start >= 0, end > start
  • Overlapping fields: two fields covering the same byte positions are rejected

On each Unmarshal call, field ranges are checked against the input line length.

Line Width

By default, Marshal pads the output to 80 characters. Use MarshalWidth to specify a different line width, or pass 0 to disable padding.

Usage

type Header struct {
    FormatCode  string `ocr:"0:2"`
    ServiceCode string `ocr:"2:4"`
    RecordType  int    `ocr:"6:8"`
}

// Unmarshal
var h Header
err := ocrline.Unmarshal("NY000010...", &h)

// Marshal
line, err := ocrline.Marshal(h)
Example (AvtaleGiroPaymentClaim)
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/karolusz/ocrline"
)

// ServiceCode is a custom type that implements Marshaler/Unmarshaler.
type ServiceCode string

func (s ServiceCode) MarshalOCR() (string, error) { return string(s), nil }
func (s *ServiceCode) UnmarshalOCR(data string) error {
	*s = ServiceCode(strings.TrimSpace(data))
	return nil
}

// RecordBase demonstrates embedded struct composition.
type RecordBase struct {
	FormatCode  string      `ocr:"0:2"`
	ServiceCode ServiceCode `ocr:"2:4"`
	TxType      string      `ocr:"4:6"`
	RecordType  int         `ocr:"6:8"`
}

// PaymentClaim demonstrates AvtaleGiro payment claim with gap fills.
// No filler fields needed - gaps are handled by implementing Filler.
type PaymentClaim struct {
	RecordBase
	TransactionNumber int    `ocr:"8:15"`
	NetsDate          string `ocr:"15:21"`

	Amount int    `ocr:"32:49"`
	KID    string `ocr:"49:74,align-right,pad-space"`
}

// OCRFill specifies that the gap at positions 21:32 should be filled with spaces.
func (r PaymentClaim) OCRFill() []ocrline.Fill {
	return []ocrline.Fill{
		{Start: 21, End: 32, Char: ' '},
	}
}

func main() {
	line := "NY2121300000001170604           00000000000000100          008000011688373000000"

	var claim PaymentClaim
	if err := ocrline.Unmarshal(line, &claim); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Service: %s\n", claim.ServiceCode)
	fmt.Printf("Transaction: %d\n", claim.TransactionNumber)
	fmt.Printf("Amount: %d øre\n", claim.Amount)
	fmt.Printf("KID: %s\n", claim.KID)
}
Output:
Service: 21
Transaction: 1
Amount: 100 øre
KID: 008000011688373
Example (CustomWidth)
package main

import (
	"fmt"
	"log"

	"github.com/karolusz/ocrline"
)

func main() {
	type ShortRecord struct {
		Code  string `ocr:"0:2"`
		Value int    `ocr:"2:10"`
	}

	r := ShortRecord{Code: "AB", Value: 42}

	// Marshal with no padding (width = 0)
	line, err := ocrline.MarshalWidth(r, 0)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("No padding: %q (len=%d)\n", line, len(line))

	// Marshal with custom width
	line, err = ocrline.MarshalWidth(r, 40)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Width 40: %q (len=%d)\n", line, len(line))
}
Output:
No padding: "AB00000042" (len=10)
Width 40: "AB00000042000000000000000000000000000000" (len=40)
Example (Marshal)
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/karolusz/ocrline"
)

// ServiceCode is a custom type that implements Marshaler/Unmarshaler.
type ServiceCode string

func (s ServiceCode) MarshalOCR() (string, error) { return string(s), nil }
func (s *ServiceCode) UnmarshalOCR(data string) error {
	*s = ServiceCode(strings.TrimSpace(data))
	return nil
}

// RecordBase demonstrates embedded struct composition.
type RecordBase struct {
	FormatCode  string      `ocr:"0:2"`
	ServiceCode ServiceCode `ocr:"2:4"`
	TxType      string      `ocr:"4:6"`
	RecordType  int         `ocr:"6:8"`
}

// TransmissionStart demonstrates a complete AvtaleGiro record.
type TransmissionStart struct {
	RecordBase
	DataTransmitter    string `ocr:"8:16"`
	TransmissionNumber string `ocr:"16:23"`
	DataRecipient      string `ocr:"23:31"`
}

func main() {
	record := TransmissionStart{
		RecordBase: RecordBase{
			FormatCode:  "NY",
			ServiceCode: "00",
			TxType:      "00",
			RecordType:  10,
		},
		DataTransmitter:    "55555555",
		TransmissionNumber: "1000081",
		DataRecipient:      "00008080",
	}

	line, err := ocrline.Marshal(&record)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(line)
}
Output:
NY000010555555551000081000080800000000000000000000000000000000000000000000000000
Example (RoundTrip)
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/karolusz/ocrline"
)

// ServiceCode is a custom type that implements Marshaler/Unmarshaler.
type ServiceCode string

func (s ServiceCode) MarshalOCR() (string, error) { return string(s), nil }
func (s *ServiceCode) UnmarshalOCR(data string) error {
	*s = ServiceCode(strings.TrimSpace(data))
	return nil
}

// RecordBase demonstrates embedded struct composition.
type RecordBase struct {
	FormatCode  string      `ocr:"0:2"`
	ServiceCode ServiceCode `ocr:"2:4"`
	TxType      string      `ocr:"4:6"`
	RecordType  int         `ocr:"6:8"`
}

// TransmissionStart demonstrates a complete AvtaleGiro record.
type TransmissionStart struct {
	RecordBase
	DataTransmitter    string `ocr:"8:16"`
	TransmissionNumber string `ocr:"16:23"`
	DataRecipient      string `ocr:"23:31"`
}

func main() {
	original := "NY000010555555551000081000080800000000000000000000000000000000000000000000000000"

	// Unmarshal
	var record TransmissionStart
	if err := ocrline.Unmarshal(original, &record); err != nil {
		log.Fatal(err)
	}

	// Marshal back
	result, err := ocrline.Marshal(&record)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result == original)
}
Output:
true
Example (Unmarshal)
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/karolusz/ocrline"
)

// ServiceCode is a custom type that implements Marshaler/Unmarshaler.
type ServiceCode string

func (s ServiceCode) MarshalOCR() (string, error) { return string(s), nil }
func (s *ServiceCode) UnmarshalOCR(data string) error {
	*s = ServiceCode(strings.TrimSpace(data))
	return nil
}

// RecordBase demonstrates embedded struct composition.
type RecordBase struct {
	FormatCode  string      `ocr:"0:2"`
	ServiceCode ServiceCode `ocr:"2:4"`
	TxType      string      `ocr:"4:6"`
	RecordType  int         `ocr:"6:8"`
}

// TransmissionStart demonstrates a complete AvtaleGiro record.
type TransmissionStart struct {
	RecordBase
	DataTransmitter    string `ocr:"8:16"`
	TransmissionNumber string `ocr:"16:23"`
	DataRecipient      string `ocr:"23:31"`
}

func main() {
	line := "NY000010555555551000081000080800000000000000000000000000000000000000000000000000"

	var record TransmissionStart
	if err := ocrline.Unmarshal(line, &record); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Format: %s\n", record.FormatCode)
	fmt.Printf("Service: %s\n", record.ServiceCode)
	fmt.Printf("Record Type: %d\n", record.RecordType)
	fmt.Printf("Transmitter: %s\n", record.DataTransmitter)
	fmt.Printf("Number: %s\n", record.TransmissionNumber)
	fmt.Printf("Recipient: %s\n", record.DataRecipient)
}
Output:
Format: NY
Service: 00
Record Type: 10
Transmitter: 55555555
Number: 1000081
Recipient: 00008080

Index

Examples

Constants

View Source
const DefaultLineWidth = 80

DefaultLineWidth is the default output line width used by Marshal.

Variables

This section is empty.

Functions

func Marshal

func Marshal(v any) (string, error)

Marshal returns the OCR line encoding of v, padded to DefaultLineWidth (80) characters.

v must be a struct or a pointer to a struct. Fields are encoded according to their `ocr` tags. Any positions not covered by struct fields are filled with '0'.

Marshal traverses embedded structs recursively.

func MarshalWidth

func MarshalWidth(v any, width int) (string, error)

MarshalWidth returns the OCR line encoding of v, padded to the specified width. If width is 0, no padding is applied and the line is exactly as wide as the rightmost field end position.

v must be a struct or a pointer to a struct.

func Unmarshal

func Unmarshal(line string, v any) error

Unmarshal parses an OCR line and stores the result in the value pointed to by v.

v must be a pointer to a struct. Fields are decoded according to their `ocr` tags. Unmarshal traverses embedded structs recursively.

Types

type Fill

type Fill struct {
	Start int
	End   int
	Char  byte
}

Fill describes a range of bytes that should be filled with a specific character during marshalling. This is used to fill gaps between fields with characters other than the default '0'.

type Filler

type Filler interface {
	OCRFill() []Fill
}

Filler is an optional interface that record structs can implement to specify how gaps (byte positions not covered by any field) should be filled during marshalling.

Gaps not covered by any Fill entry default to '0'.

Example:

func (r PaymentClaim) OCRFill() []ocrline.Fill {
    return []ocrline.Fill{
        {Start: 21, End: 32, Char: ' '},
    }
}

type InvalidMarshalError

type InvalidMarshalError struct {
	Type reflect.Type
}

InvalidMarshalError describes an invalid argument passed to Marshal.

func (*InvalidMarshalError) Error

func (e *InvalidMarshalError) Error() string

type InvalidUnmarshalError

type InvalidUnmarshalError struct {
	Type reflect.Type
}

InvalidUnmarshalError describes an invalid argument passed to Unmarshal.

func (*InvalidUnmarshalError) Error

func (e *InvalidUnmarshalError) Error() string

type MarshalFieldError

type MarshalFieldError struct {
	Field string
	Err   error
}

MarshalFieldError describes an error marshalling a specific field.

func (*MarshalFieldError) Error

func (e *MarshalFieldError) Error() string

func (*MarshalFieldError) Unwrap

func (e *MarshalFieldError) Unwrap() error

type Marshaler

type Marshaler interface {
	MarshalOCR() (string, error)
}

Marshaler is the interface implemented by types that can marshal themselves into a fixed-width OCR field string.

type OverlapError

type OverlapError struct {
	Field1       string
	Start1, End1 int
	Field2       string
	Start2, End2 int
}

OverlapError describes two fields whose ocr tag ranges overlap.

func (*OverlapError) Error

func (e *OverlapError) Error() string

type TagError

type TagError struct {
	Field string
	Tag   string
	Err   error
}

TagError describes an error in an ocr struct tag.

func (*TagError) Error

func (e *TagError) Error() string

func (*TagError) Unwrap

func (e *TagError) Unwrap() error

type UnmarshalFieldError

type UnmarshalFieldError struct {
	Field string
	Err   error
}

UnmarshalFieldError describes an error unmarshalling a specific field.

func (*UnmarshalFieldError) Error

func (e *UnmarshalFieldError) Error() string

func (*UnmarshalFieldError) Unwrap

func (e *UnmarshalFieldError) Unwrap() error

type UnmarshalRangeError

type UnmarshalRangeError struct {
	Field     string
	Start     int
	End       int
	LineWidth int
}

UnmarshalRangeError describes a field whose ocr tag range exceeds the input line.

func (*UnmarshalRangeError) Error

func (e *UnmarshalRangeError) Error() string

type Unmarshaler

type Unmarshaler interface {
	UnmarshalOCR(data string) error
}

Unmarshaler is the interface implemented by types that can unmarshal a fixed-width OCR field string into themselves.

Jump to

Keyboard shortcuts

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