edifact

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: MIT Imports: 10 Imported by: 0

README

edifact

A Go library for reading and writing UN/EDIFACT interchanges, the message format ISO 9735 defines.

go get github.com/MaldivesPorts/edifact

It needs Go 1.22 or later and has no dependencies outside the standard library.

Quick start

Read a file

bayplan.edi holds the short bayplan from EDIFACT in brief.

f, err := os.Open("bayplan.edi")
if err != nil {
	log.Fatal(err)
}
defer f.Close()

ix, err := edifact.Read(f)
if err != nil {
	log.Fatal(err)
}

fmt.Println("from", ix.Sender(), "to", ix.Recipient())
for _, m := range ix.Messages() {
	fmt.Println("message", m.Key())
	for _, eqd := range m.FindAll("EQD", "CN") {
		fmt.Println("container", eqd.Element(2), "size and type", eqd.Element(3))
	}
}
from SENDER to RECIPIENT
message BAPLIE:D:95B:UN:-
container XXXU1234567 size and type 22G1
container XXXU7654321 size and type 45G1
Write a file
ix := edifact.NewInterchange()
ix.SetSender("SENDER", "")
ix.SetRecipient("RECIPIENT", "")
ix.SetPreparedDatetime("260101", "1200")
ix.SetControlRef("REF1")

m := ix.AddMessage(edifact.Key{
	Type:    edifact.Is("BAPLIE"),
	Version: edifact.Is("D"),
	Release: edifact.Is("95B"),
	Agency:  edifact.Is("UN"),
})
m.SetRef("M1")
m.Add("BGM", "", "BAY1", "9")
m.AddComposite("DTM", []string{"137", "202601011200", "203"})
m.AddComposite("LOC", []string{"147"}, []string{"0010204", "", "5"})
m.Add("EQD", "CN", "XXXU1234567", "22G1")

if err := edifact.Write(os.Stdout, ix, edifact.SegmentSuffixLF()); err != nil {
	log.Fatal(err)
}
UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
DTM+137:202601011200:203'
LOC+147+0010204::5'
EQD+CN+XXXU1234567+22G1'
UNT+6+M1'
UNZ+1+REF1'

Write fills in the UNA, UNT and UNZ lines, including their counts.

Documentation

  • EDIFACT in brief introduces the format, as well as the terms used by this package.
  • The package documentation covers more options (including defining specs and validating against them), with runnable examples.

Known limitations/room for improvement

  • Batch EDIFACT only. Interactive EDIFACT (UIB to UIZ) and the syntax version 4 package segments (UNO, UNP) are not read or written.
  • Character sets. Read and Write pass bytes through unchanged. They neither convert from or to the character set the syntax identifier names, such as ISO 8859-1 for UNOC, nor check values against it.
  • Values are not checked against the directory. A specification describes segment order, repetition and qualifiers. It does not check element lengths, formats, code list values, or which data elements a segment requires.
  • Header checks cover presence only. Write refuses empty mandatory header elements but not ones that are too long or in the wrong format, such as a five-digit date.
  • Explicit segment indices go unchecked. DDD:1:2 survives a read and a write, but validation does not compare the numbers with the nesting a specification describes.
  • Date formats. Only the codes listed under Numbers and dates are supported. Time zones written as letters must be GMT or UTC.
  • Whole interchanges in memory. Read builds the complete interchange before returning, and Write renders all of it before writing. A streaming reader/writer would fix this.
  • Round trips change some bytes. See Reading and writing back.

License

MIT. See LICENSE.

Documentation

Overview

Package edifact reads and writes UN/EDIFACT interchanges, as defined by ISO 9735.

An Interchange holds messages, and each Message holds segments. Positions in a Segment count from 1, the way the published segment tables number them. For readers new to the format, EDIFACT in brief walks through a short file and defines the terms used here.

ix, err := edifact.Read(r)
if err != nil {
	log.Fatal(err)
}
for _, eqd := range ix.Message(1).FindAll("EQD", "CN") {
	fmt.Println(eqd.Element(2))
}

Reading

Read reads the envelope and segments without consulting any specification. Envelope values have named accessors, such as Interchange.Sender and Message.Key. To read a position that has no named accessor, Interchange.Header, FunctionalGroup.Header and Message.Header return the UNB, UNG and UNH segments themselves.

Message.Find returns the first segment with a tag and qualifier, and Message.FindAll every one. An empty qualifier matches any. Segment.Element, Segment.Component and Segment.Components read positions. An absent position, or any position of a missing segment, reads as "", so a chain such as m.Find("RFF", "BM").Element(2) never panics.

Numbers and dates

Segment.Float accepts a dot or a comma as the decimal mark, a leading decimal mark (.5) and an exponent (1E3). Segment.Int accepts an optional minus sign and digits. Both return ErrNumeric for anything else, such as +5, 2., 1,000.5 or NaN, and ErrEmpty for an empty or absent position.

Segment.Time reads a date in the format named by the component after it, the layout DTM uses. Segment.TimeIn takes the format as an argument, and ParseTime works on a plain string. These codes from code list 2379 are supported:

2    DDMMYY                301  YYMMDDHHMMZZZ
3    MMDDYY                401  HHMM
101  YYMMDD                402  HHMMSS
102  CCYYMMDD              404  HHMMSSZZZ
201  YYMMDDHHMM            602  CCYY
202  YYMMDDHHMMSS          610  CCYYMM
203  CCYYMMDDHHMM          718  CCYYMMDD-CCYYMMDD
204  CCYYMMDDHHMMSS        719  CCYYMMDDHHMM-CCYYMMDDHHMM
205  CCYYMMDDHHMMZHHMM

ZZZ is a time zone written as letters. Time reads GMT and UTC as UTC, and returns ErrDateZone for any other zone. A time from a format without a zone keeps its digits as written and comes back in UnknownTimezone, so a caller can attach the right zone. Segment.TimeRange and ParseTimeRange read the range formats 718 and 719.

Filler

Read skips whitespace, control characters such as STX and ETX, and a UTF-8 byte order mark before the first segment or after the last. It also skips line breaks between segments, so files with one segment per line read the same as files with none.

Errors and diagnostics

Read returns an error only when it cannot carry on:

  • no UNB at the start, or a UNA with missing or repeated separators
  • a service segment out of place, such as UNH inside a message
  • a message or functional group still open at the end
  • a segment with no terminator, or one longer than MaxSegmentSize
  • anything after UNZ

By default, Read collects everything else on Interchange.Diagnostics and succeeds:

  • a UNT, UNE or UNZ count or reference that does not match
  • a header missing mandatory elements
  • no UNZ
  • some messages in functional groups and some not
  • an interchange or functional group with no messages, or a message with no segments
  • below syntax version 4, a message whose type, version, release or agency differs from its functional group's
  • every problem a specification finds

Each Diagnostic gives its Severity, the message it concerns and the segment.

Read options

Read, ReadWithSpec and Registry.Read take options after their other arguments:

  • RejectReadOn returns the first diagnostic at or above a severity as an error. Default: none.
  • MaxSegmentSize limits the length of one segment. Default: 16 MiB.
  • DefaultDelimiters gives the separators for a file with no UNA. Default: the level A separators.

Specifications and segment groups

Without a specification a message is a flat list of segments. A Spec describes one message type's segment table, so reading can sort the segments into SegmentGroup views and report what does not fit. The library ships no specs. Callers write them for the messages they handle.

A segment group is a view over the message's own segments, so a segment found through one is the same segment Message.Segments holds. Message.FindGroup returns the first repetition of a segment group and Message.FindAllGroups every one. Segment groups nest, and the same methods work at every level. On a missing message or segment group they return nil, so a chain ending in Element gives "" rather than panicking.

A Node is either a segment, named by Tag, or a segment group, named by Group and holding Nodes. Qualifier is the value element 1 must have, and Qualifiers lists several accepted in any order. Required means at least once, and for a node with Qualifiers any one of them is enough. Max is how often it may appear: 0 means once and Unbounded means without limit.

A spec may declare the same tag in several nodes with different qualifiers, and the same segment at two positions with other nodes between them. Registry.Register refuses a segment declared twice in a row, since Max covers that. Validation reports a warning for a segment that matches nothing where it appears, and for one repeated more often than its Max allows. Either stays in the view of the segment group it falls inside. Validation reports an error for an absent required segment or segment group.

Spec.Clone copies a spec deeply, to derive a variant without touching the original.

Keys and the registry

A Key selects a spec from a message's UNH. Its fields are the data elements 0065 message type, 0052 version, 0054 release, 0051 controlling agency and 0057 association assigned code. Is matches a value exactly, a nil field matches anything, and Absent matches only an empty component. When several specs match a message, the one constraining the most fields wins, and a later registration beats an earlier one with the same fields.

A Registry holds specs. Registry.Register adds one and Registry.Load a slice. Registry.Validate checks an interchange built in code and fills in its segment groups, as Registry.Read does.

Choosing which specs apply

The library keeps no registry between calls. Every read or write names the specs it uses:

Read                        Write                            Specs used
edifact.Read(r)             edifact.Write(w, ix)             none
reg.Read(r)                 reg.Write(w, ix)                 those in reg
edifact.ReadWithSpec(r, s)  edifact.WriteWithSpec(w, ix, s)  s alone

Nothing is shared between registries, so separate parts of a program, or separate tests, can each use their own. Build a registry fully before reading with it. After that, any number of goroutines can read and write through it.

In a program that reads many files, build the registry once in a package of its own and call Read through a small function there:

// Package messages reads the EDIFACT messages this program handles.
package messages

var registry = sync.OnceValues(func() (*edifact.Registry, error) {
	reg := edifact.NewRegistry()
	return reg, reg.Load(specs)
})

func Read(r io.Reader, opts ...edifact.ReadOption) (*edifact.Interchange, error) {
	reg, err := registry()
	if err != nil {
		return nil, err
	}
	return reg.Read(r, opts...)
}

Writing

NewInterchange returns an interchange with syntax UNOA version 2. Set the UNB values with its setters, then add messages with Interchange.AddMessage. Message.Add builds a segment from simple elements and Message.AddComposite from a slice of components per element. Segment.Set, Segment.SetInt, Segment.SetFloat and Segment.SetTime write one component of an existing segment, extending it as needed.

For formats 301 and 404, whose zone is written as letters, SetTime converts the time to UTC and writes UTC. For other formats it writes the time's own clock, and for 205 its own offset as well.

Interchange.AddMessageTo puts a message in a functional group from Interchange.AddFunctionalGroup. Write writes functional groups in the order they were added, each with its messages, and UNZ then counts functional groups rather than messages.

Message.Add appends to the end of the message. SegmentGroup.Add and SegmentGroup.AddGroup place each segment where its segment group belongs, even after later segment groups exist.

Write:

  • computes the UNT, UNE and UNZ counts and references
  • always writes a UNA declaring Interchange.Delimiters, with a comma as the decimal mark if Delimiters.Decimal is one and a dot otherwise. SetFloat values use that mark. Values set as text keep theirs.
  • escapes separators inside values with the release character
  • drops trailing empty components and elements and trailing spaces, as ISO 9735 requires
  • returns ErrIncompleteHeader for a UNB, UNG or UNH missing an element ISO 9735 makes mandatory, ErrNoContent for an interchange or functional group with no messages or a message with no segments, and ErrGroupMismatch below syntax version 4 for a message that differs from its functional group
  • writes nothing if any of this fails

Registry.Write and WriteWithSpec validate every message first. They drop a segment with no data that the spec marks conditional, and write a mandatory one as its bare tag. Without a spec, Write writes every segment and validates nothing.

Write options

Reading and writing back

Reading a file and writing it back gives the same data, though not always the same bytes. Write drops line breaks, trailing spaces, trailing empty elements, and segments with no data that the spec marks conditional. It adds a UNA if there was none, and recomputes the trailer counts.

A file without a UNA may use a comma as the decimal mark, and Write declares a dot in the UNA it adds. The library cannot tell which values are numbers, so it leaves them as they are. Read the positions known to be numeric with Float and write them back with SetFloat.

Separators and syntax levels

The UNA declares six characters, in order: component separator, element separator, decimal mark, release character, repetition separator and segment terminator. Interchange keeps what was read in its Delimiters field. A space in the release position means there is none, and writing a value that holds a separator then returns ErrUnreleasable.

The first component of UNB names the syntax level, mostly a character set. Read reads any level when the file starts with a UNA. Without one, it understands the level A separators ' + : ?, plus * as the repetition separator in syntax version 4, or the separators given with DefaultDelimiters. A file without a UNA that uses the level B separators otherwise gives ErrUnsupportedSeparators.

Repetitions

From syntax version 4, one element can hold several values, separated by the repetition separator. Segment.Repetitions counts them and Segment.Repetition reads one. Element, Component, Components and Set work on the first repetition, so code that ignores repetitions keeps working. Segment.SetRepetition writes into one.

To write repetitions, call Interchange.SetSyntax with version 4. Syntax versions 1 to 3 have no repetitions. The fifth UNA position is a space, * is an ordinary character, and writing an element with several repetitions returns ErrNoRepetitionSeparator.

Explicit segment indices

A tag can carry its nesting level and repetition number as components, such as DDD:1:2. Read takes the tag as DDD, so specs match it, and Segment.Index returns the numbers. Segment.SetIndex sets them.

Example

Reading a file and pulling values out by position.

package main

import (
	"fmt"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

// bayplan is a short interchange cut down from a BAPLIE bayplan: two
// containers in their stowage cells. Newlines between segments are optional
// and ignored on read.
const bayplan = `UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
DTM+137:202601011200:203'
LOC+147+0010204::5'
MEA+WT++KGM:12000'
LOC+9+XXAAA:139:6'
LOC+11+XXBBB:139:6'
EQD+CN+XXXU1234567+22G1'
LOC+147+0010206::5'
MEA+WT++KGM:8500.5'
LOC+9+XXAAA:139:6'
LOC+11+XXCCC:139:6'
EQD+CN+XXXU7654321+45G1'
UNT+14+M1'
UNZ+1+REF1'`

func main() {
	ix, err := edifact.Read(strings.NewReader(bayplan))
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("from", ix.Sender(), "to", ix.Recipient())
	m := ix.Message(1)
	fmt.Println("message", m.Key())
	for _, eqd := range m.FindAll("EQD", "CN") {
		fmt.Println("container", eqd.Element(2), "size and type", eqd.Element(3))
	}
}
Output:
from SENDER to RECIPIENT
message BAPLIE:D:95B:UN:-
container XXXU1234567 size and type 22G1
container XXXU7654321 size and type 45G1

Index

Examples

Constants

View Source
const Unbounded = -1

Unbounded lets a Node repeat without limit.

Variables

View Source
var (
	ErrNoUNB      = errors.New("edifact: interchange does not open with UNB")
	ErrUnexpected = errors.New("edifact: service segment out of place")
	ErrUnclosed   = errors.New("edifact: interchange ends with a message or functional group still open")
)

Envelope structure the reader cannot carry on from.

View Source
var ErrDateFormat = errors.New("edifact: unknown date format qualifier")

ErrDateFormat reports a date format qualifier the package does not decode.

View Source
var ErrDateRange = errors.New("edifact: date format qualifier names a range")

ErrDateRange reports a date format qualifier naming a period rather than a single point in time.

View Source
var ErrDateZone = errors.New("edifact: unsupported time zone")

ErrDateZone reports a time zone, written as letters, that is neither GMT nor UTC. The code list does not define the zone's format, so nothing else is guessed at.

View Source
var ErrEmpty = errors.New("edifact: value is empty")

ErrEmpty reports a position that holds no value, which is distinct from one holding a value that will not convert.

View Source
var ErrGroupMismatch = errors.New("edifact: message does not match its functional group")

ErrGroupMismatch reports a message whose type, version, release or controlling agency differs from its functional group's, which syntax versions 1 to 3 forbid.

View Source
var ErrIncompleteHeader = errors.New("edifact: header is missing mandatory elements")

ErrIncompleteHeader reports a UNB, UNG or UNH missing an element ISO 9735 makes mandatory.

View Source
var ErrInvalidDelimiters = errors.New("edifact: invalid delimiters")

ErrInvalidDelimiters reports a component separator, element separator or segment terminator that is zero, or two delimiters that are the same character.

View Source
var ErrNoContent = errors.New("edifact: nothing to enclose")

ErrNoContent reports an interchange with no messages, a functional group with no messages, or a message with no segments between UNH and UNT.

View Source
var ErrNoRepetitionSeparator = errors.New("edifact: element repeats and there is no repetition separator")

ErrNoRepetitionSeparator reports an element with several repetitions, written to an interchange that declares no repetition separator.

View Source
var ErrNumeric = errors.New("edifact: not a valid EDIFACT numeric value")

ErrNumeric reports a value that is not an ISO 9735 numeric: an optional minus sign, digits with at most one decimal mark followed by a digit, and an optional exponent such as E3 or e-3.

View Source
var ErrSegmentTooLong = errors.New("edifact: segment is too long")

ErrSegmentTooLong reports a segment longer than MaxSegmentSize allows.

View Source
var ErrShortUNA = errors.New("edifact: UNA segment is too short")

ErrShortUNA reports a UNA segment with fewer than six characters after the tag.

View Source
var ErrUnreleasable = errors.New("edifact: value contains a separator and there is no release character")

ErrUnreleasable reports a value containing a separator, written to an interchange that has no release character to escape it with.

View Source
var ErrUnsupportedSeparators = errors.New("edifact: default separators other than level A need a UNA or DefaultDelimiters")

ErrUnsupportedSeparators reports an interchange with no UNA whose UNB uses the information separators IS3 and IS1, the defaults of syntax level B. Without a UNA, Read uses the level A separators unless DefaultDelimiters names others.

View Source
var ErrUnterminated = errors.New("edifact: segment is not terminated")

ErrUnterminated reports trailing content with no segment terminator.

View Source
var UnknownTimezone = time.FixedZone("UNKNOWN", 0)

UnknownTimezone is the location of a time read from a date format that carries no time zone. Its digits are as written and its offset is zero, which says nothing about the zone the sender meant.

Functions

func Absent

func Absent() *string

Absent returns a Key field matching only an empty component, as distinct from a nil field, which matches anything.

func Is

func Is(v string) *string

Is returns a Key field matching exactly v.

func ParseTime

func ParseTime(value, format string) (time.Time, error)

ParseTime decodes a value against a date format qualifier from code list 2379. A format with no time zone gives a time in UnknownTimezone. A zone written as GMT or UTC gives UTC, and an offset gives that offset.

Two-digit years follow the Go convention, so 00 to 68 read as 2000 to 2068 and 69 to 99 as 1969 to 1999.

func ParseTimeRange

func ParseTimeRange(value, format string) (start, end time.Time, err error)

ParseTimeRange decodes a value against a format qualifier naming a period, such as 718 or 719. The two halves may be joined by a hyphen or run together.

func Write

func Write(w io.Writer, ix *Interchange, opts ...WriteOption) error

Write serialises ix without validating it against any spec. ix is left as it was, including its messages' segment groups. To validate first, use a Registry's Write or WriteWithSpec.

Write always emits a UNA declaring ix.Delimiters.

Example

Building an interchange and writing it.

package main

import (
	"fmt"
	"os"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	ix := edifact.NewInterchange()
	ix.SetSender("SENDER", "")
	ix.SetRecipient("RECIPIENT", "")
	ix.SetPreparedDatetime("260101", "1200")
	ix.SetControlRef("REF1")

	m := ix.AddMessage(edifact.Key{
		Type:    edifact.Is("BAPLIE"),
		Version: edifact.Is("D"),
		Release: edifact.Is("95B"),
		Agency:  edifact.Is("UN"),
	})
	m.SetRef("M1")
	m.Add("BGM", "", "BAY1", "9")
	m.AddComposite("DTM", []string{"137", "202601011200", "203"})
	m.AddComposite("LOC", []string{"147"}, []string{"0010204", "", "5"})
	m.Add("EQD", "CN", "XXXU1234567", "22G1")

	if err := edifact.Write(os.Stdout, ix, edifact.SegmentSuffixLF()); err != nil {
		fmt.Println(err)
	}
}
Output:
UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
DTM+137:202601011200:203'
LOC+147+0010204::5'
EQD+CN+XXXU1234567+22G1'
UNT+6+M1'
UNZ+1+REF1'

func WriteWithSpec

func WriteWithSpec(w io.Writer, ix *Interchange, s Spec, opts ...WriteOption) error

WriteWithSpec is Write with every message validated against s alone.

Types

type Delimiters

type Delimiters struct {
	// Component separates the components of a composite element, ':' by
	// default.
	Component byte
	// Element separates a segment's elements, '+' by default.
	Element byte
	// Decimal records what a UNA declared. Reading accepts either a dot or a
	// comma whatever it says. Writing declares a comma when Decimal is one, and
	// a dot otherwise.
	Decimal byte
	// Release is zero when the interchange declares no release character,
	// which a UNA does by putting a space in that position.
	Release byte
	// Repetition separates the repetitions of an element in syntax version 4,
	// '*' by default. It is zero below version 4, which has no repetitions.
	Repetition byte
	// Segment ends each segment, '\” by default.
	Segment byte
}

Delimiters are the six characters declared by a UNA segment, in UNA order.

func ParseUNA

func ParseUNA(b []byte) (Delimiters, bool, error)

ParseUNA reads delimiters from b when it opens with a UNA segment. The second result is false when it does not.

func StandardDelimiters

func StandardDelimiters() Delimiters

StandardDelimiters returns the defaults used by this package.

  • Repetition is zero. Write uses an asterisk for syntax version 4.
  • Decimal is '.', the library's own default. ISO 9735 allows a dot or a comma.

type Diagnostic

type Diagnostic struct {
	// Severity is Warning or Error.
	Severity Severity
	// MessageIdx is the message's position in the interchange, counting from
	// 1, or 0 when the diagnostic concerns the envelope.
	MessageIdx int
	// MessageRef is the message's UNH reference, which need not be unique.
	MessageRef string
	// Path names the segment groups enclosing the problem, such as /SG2/SG3,
	// and is empty at the top level of a message or for the envelope.
	Path string
	// Tag is the segment tag, or the segment group's name when a whole
	// segment group is at fault.
	Tag string
	// Qualifier is element 1 of the segment, or the qualifier a spec expected.
	Qualifier string
	// SegmentIdx counts from 1, starting at UNH when MessageIdx is set and at
	// UNB otherwise. It is 0 when no single segment is at fault. Within a
	// message, segment n is Segments()[n-2], with UNH at 1 and UNT one past the
	// last of Segments().
	SegmentIdx int
	// Message describes the problem.
	Message string
}

Diagnostic locates a structural violation within an interchange.

Example

Read reports what does not fit the specification as diagnostics rather than failing, so a partly wrong file can still be read.

package main

import (
	"fmt"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

// bayplan is a short interchange cut down from a BAPLIE bayplan: two
// containers in their stowage cells. Newlines between segments are optional
// and ignored on read.
const bayplan = `UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
DTM+137:202601011200:203'
LOC+147+0010204::5'
MEA+WT++KGM:12000'
LOC+9+XXAAA:139:6'
LOC+11+XXBBB:139:6'
EQD+CN+XXXU1234567+22G1'
LOC+147+0010206::5'
MEA+WT++KGM:8500.5'
LOC+9+XXAAA:139:6'
LOC+11+XXCCC:139:6'
EQD+CN+XXXU7654321+45G1'
UNT+14+M1'
UNZ+1+REF1'`

// bayplanSpec is a simplified specification after the BAPLIE D.95B segment
// table, which has more segments than this: a header, then one SG2 per stowage
// cell, each holding an SG3 for the container in it.
func bayplanSpec() edifact.Spec {
	return edifact.Spec{
		Key: edifact.Key{
			Type:    edifact.Is("BAPLIE"),
			Version: edifact.Is("D"),
			Release: edifact.Is("95B"),
			Agency:  edifact.Is("UN"),
		},
		Nodes: []edifact.Node{
			{Tag: "BGM", Required: true},
			{Tag: "DTM", Qualifier: "137", Required: true},
			{Group: "SG2", Max: edifact.Unbounded, Nodes: []edifact.Node{
				{Tag: "LOC", Qualifier: "147", Required: true},
				{Tag: "MEA", Qualifier: "WT"},
				{Tag: "LOC", Qualifiers: []string{"9", "11"}, Max: 9},
				{Group: "SG3", Max: 3, Nodes: []edifact.Node{
					{Tag: "EQD", Qualifier: "CN", Required: true},
				}},
			}},
		},
	}
}

func main() {
	reg := edifact.NewRegistry()
	if err := reg.Register(bayplanSpec()); err != nil {
		fmt.Println(err)
		return
	}

	broken := strings.Replace(bayplan, "DTM+137:202601011200:203'", "FTX+AAI+++NOTE'", 1)
	ix, err := reg.Read(strings.NewReader(broken))
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, d := range ix.Diagnostics {
		fmt.Println(d)
	}
}
Output:
warning: message 1 (M1), segment 3, FTX+AAI: segment is not declared here
error: message 1 (M1), segment 4, DTM+137: required segment is missing

func (Diagnostic) String

func (d Diagnostic) String() string

String renders the diagnostic on one line, such as "error: message 1 (M1), segment 4, DTM+137: required segment is missing".

type FunctionalGroup

type FunctionalGroup struct {
	// contains filtered or unexported fields
}

FunctionalGroup is the UNG and UNE pair bracketing a set of messages. It holds no messages. Each message points at its functional group.

func (*FunctionalGroup) Agency

func (g *FunctionalGroup) Agency() string

Agency returns the controlling agency.

func (*FunctionalGroup) ControlRef

func (g *FunctionalGroup) ControlRef() string

ControlRef returns the functional group's reference, which UNE repeats.

func (*FunctionalGroup) Date

func (g *FunctionalGroup) Date() string

Date returns the date of preparation, as YYMMDD or CCYYMMDD.

func (*FunctionalGroup) Header

func (g *FunctionalGroup) Header() *Segment

Header returns the UNG segment, creating an empty one if there is none. A nil g gives nil.

func (*FunctionalGroup) MessageType

func (g *FunctionalGroup) MessageType() string

MessageType returns the message type the functional group names. Below syntax version 4, every message in the functional group must be of this type.

func (*FunctionalGroup) Recipient

func (g *FunctionalGroup) Recipient() string

Recipient returns the functional group's recipient identification.

func (*FunctionalGroup) Release

func (g *FunctionalGroup) Release() string

Release returns the message release number.

func (*FunctionalGroup) Sender

func (g *FunctionalGroup) Sender() string

Sender returns the functional group's sender identification.

func (*FunctionalGroup) SetAgency

func (g *FunctionalGroup) SetAgency(a string)

SetAgency writes the controlling agency.

func (*FunctionalGroup) SetControlRef

func (g *FunctionalGroup) SetControlRef(ref string)

SetControlRef writes the functional group's reference.

func (*FunctionalGroup) SetMessageType

func (g *FunctionalGroup) SetMessageType(t string)

SetMessageType writes the message type the functional group names.

func (*FunctionalGroup) SetPreparedDatetime

func (g *FunctionalGroup) SetPreparedDatetime(date, time string)

SetPreparedDatetime writes the date and time of preparation.

func (*FunctionalGroup) SetRecipient

func (g *FunctionalGroup) SetRecipient(id, qualifier string)

SetRecipient writes the functional group's recipient identification and its qualifier.

func (*FunctionalGroup) SetSender

func (g *FunctionalGroup) SetSender(id, qualifier string)

SetSender writes the functional group's sender identification and its qualifier.

func (*FunctionalGroup) SetVersionRelease

func (g *FunctionalGroup) SetVersionRelease(version, release string)

SetVersionRelease writes the message version and release numbers, which name the directory together.

func (*FunctionalGroup) Time

func (g *FunctionalGroup) Time() string

Time returns the time of preparation, as HHMM.

func (*FunctionalGroup) Version

func (g *FunctionalGroup) Version() string

Version returns the message version number.

type Interchange

type Interchange struct {

	// Delimiters are the separators read from the UNA, or the defaults, and
	// the ones Write uses.
	Delimiters Delimiters
	// Diagnostics are the problems Read found that did not stop it.
	Diagnostics []Diagnostic
	// contains filtered or unexported fields
}

Interchange is everything between UNB and UNZ.

func NewInterchange

func NewInterchange() *Interchange

NewInterchange returns an empty interchange with the standard delimiters and a UNB naming syntax UNOA version 2.

func Read

func Read(r io.Reader, opts ...ReadOption) (*Interchange, error)

Read parses an interchange without consulting any spec, so the result carries the envelope's own diagnostics and nothing about message structure. The error covers syntax alone. Anything structural is collected onto the returned interchange's Diagnostics, unless RejectReadOn turns it into an error. To check messages against specs, use Registry.Read or ReadWithSpec.

func ReadWithSpec

func ReadWithSpec(r io.Reader, s Spec, opts ...ReadOption) (*Interchange, error)

ReadWithSpec parses an interchange, checking each message against s alone. A message whose UNH key s does not match is reported and left ungrouped.

func (*Interchange) AddFunctionalGroup

func (ix *Interchange) AddFunctionalGroup(messageType string) *FunctionalGroup

AddFunctionalGroup appends a functional group whose UNG names messageType, and returns it.

Example

A message joins a functional group when it is created. With functional groups, UNZ counts functional groups rather than messages.

package main

import (
	"fmt"
	"os"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	ix := edifact.NewInterchange()
	ix.SetSender("SENDER", "")
	ix.SetRecipient("RECIPIENT", "")
	ix.SetPreparedDatetime("260101", "1200")
	ix.SetControlRef("REF1")

	g := ix.AddFunctionalGroup("BAPLIE")
	g.SetSender("SENDER", "")
	g.SetRecipient("RECIPIENT", "")
	g.SetPreparedDatetime("260101", "1200")
	g.SetControlRef("G1")
	g.SetAgency("UN")
	g.SetVersionRelease("D", "95B")

	m := ix.AddMessageTo(g, edifact.Key{
		Type:    edifact.Is("BAPLIE"),
		Version: edifact.Is("D"),
		Release: edifact.Is("95B"),
		Agency:  edifact.Is("UN"),
	})
	m.SetRef("M1")
	m.Add("BGM", "", "BAY1", "9")

	if err := edifact.Write(os.Stdout, ix, edifact.SegmentSuffixLF()); err != nil {
		fmt.Println(err)
	}
}
Output:
UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNG+BAPLIE+SENDER+RECIPIENT+260101:1200+G1+UN+D:95B'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
UNT+3+M1'
UNE+1+G1'
UNZ+1+REF1'

func (*Interchange) AddMessage

func (ix *Interchange) AddMessage(k Key) *Message

AddMessage appends a message outside any functional group whose UNH names k, and returns it.

func (*Interchange) AddMessageTo

func (ix *Interchange) AddMessageTo(g *FunctionalGroup, k Key) *Message

AddMessageTo appends a message in functional group g whose UNH names k, and returns it. A nil g adds the message outside any functional group.

func (*Interchange) AgreementID

func (ix *Interchange) AgreementID() string

AgreementID returns the interchange agreement identifier.

func (*Interchange) ApplicationRef

func (ix *Interchange) ApplicationRef() string

ApplicationRef returns the application reference.

func (*Interchange) ControlRef

func (ix *Interchange) ControlRef() string

ControlRef returns the interchange control reference, which UNZ repeats.

func (*Interchange) Date

func (ix *Interchange) Date() string

Date returns the date of preparation, as YYMMDD or CCYYMMDD.

func (*Interchange) Grouped

func (ix *Interchange) Grouped() bool

Grouped reports whether every message belongs to a functional group.

func (*Interchange) Groups

func (ix *Interchange) Groups() []*FunctionalGroup

Groups returns the functional groups in write order, or nil when there are none. The slice is a copy.

func (*Interchange) Header

func (ix *Interchange) Header() *Segment

Header returns the UNB segment, creating an empty one if there is none.

func (*Interchange) IsTest

func (ix *Interchange) IsTest() bool

IsTest reports whether the test indicator is set.

func (*Interchange) Message

func (ix *Interchange) Message(n int) *Message

Message returns message n, counting from 1, or nil when there is none.

func (*Interchange) Messages

func (ix *Interchange) Messages() []*Message

Messages returns the messages in write order. The slice is a copy.

func (*Interchange) Recipient

func (ix *Interchange) Recipient() string

Recipient returns the interchange recipient identification.

func (*Interchange) Sender

func (ix *Interchange) Sender() string

Sender returns the interchange sender identification.

func (*Interchange) SetAgreementID

func (ix *Interchange) SetAgreementID(id string)

SetAgreementID writes the interchange agreement identifier.

func (*Interchange) SetApplicationRef

func (ix *Interchange) SetApplicationRef(ref string)

SetApplicationRef writes the application reference.

func (*Interchange) SetControlRef

func (ix *Interchange) SetControlRef(ref string)

SetControlRef writes the interchange control reference.

func (*Interchange) SetPreparedDatetime

func (ix *Interchange) SetPreparedDatetime(date, time string)

SetPreparedDatetime writes the date and time of preparation.

func (*Interchange) SetRecipient

func (ix *Interchange) SetRecipient(id, qualifier string)

SetRecipient writes the recipient identification and its qualifier.

func (*Interchange) SetSender

func (ix *Interchange) SetSender(id, qualifier string)

SetSender writes the sender identification and its qualifier.

func (*Interchange) SetSyntax

func (ix *Interchange) SetSyntax(identifier, version string)

SetSyntax writes the character repertoire and the ISO 9735 edition.

func (*Interchange) SetTest

func (ix *Interchange) SetTest(test bool)

SetTest marks the interchange as test data.

func (*Interchange) SyntaxIdentifier

func (ix *Interchange) SyntaxIdentifier() string

SyntaxIdentifier returns the character repertoire, such as UNOA.

func (*Interchange) SyntaxVersion

func (ix *Interchange) SyntaxVersion() string

SyntaxVersion returns the ISO 9735 edition, "1" through "4".

func (*Interchange) Time

func (ix *Interchange) Time() string

Time returns the time of preparation, as HHMM.

type Key

type Key struct {
	Type        *string // 0065 message type
	Version     *string // 0052 message version number
	Release     *string // 0054 message release number
	Agency      *string // 0051 controlling agency
	Association *string // 0057 association assigned code
}

Key identifies a message specification. It is the composite carried by UNH, element 2.

A nil field matches any value. A non-nil field must equal it exactly, so Absent matches only an empty component. A Key read off the wire never holds nil.

Example

A nil Key field matches anything. Absent matches only an empty component, which tells a plain UN message from an industry variant of it.

package main

import (
	"fmt"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	anyBaplie := edifact.Key{Type: edifact.Is("BAPLIE")}
	plain := edifact.Key{
		Type:        edifact.Is("BAPLIE"),
		Version:     edifact.Is("D"),
		Release:     edifact.Is("95B"),
		Agency:      edifact.Is("UN"),
		Association: edifact.Absent(),
	}

	fmt.Println(anyBaplie)
	fmt.Println(plain)
}
Output:
BAPLIE:*:*:*:*
BAPLIE:D:95B:UN:-

func (Key) Equal

func (k Key) Equal(other Key) bool

Equal reports whether two Keys constrain the same things. Key holds pointers, so == compares addresses rather than values.

func (Key) String

func (k Key) String() string

String renders the Key colon-separated, with * for a field matching anything and - for one matching only an empty component.

type Message

type Message struct {
	// contains filtered or unexported fields
}

Message is everything between UNH and UNT.

func (*Message) Add

func (m *Message) Add(tag string, elems ...string) *Segment

Add appends a segment built from elements, one component each, and returns it.

func (*Message) AddComposite

func (m *Message) AddComposite(tag string, elems ...[]string) *Segment

AddComposite appends a segment whose elements each carry several components. The segment holds copies of elems.

func (*Message) AddGroup

func (m *Message) AddGroup(name string) *SegmentGroup

AddGroup appends a segment group. name is the label used by the published tables, such as "SG2".

func (*Message) Find

func (m *Message) Find(tag, qualifier string) *Segment

Find returns the first segment with the given tag whose element 1 matches qualifier, or nil. An empty qualifier matches any. A nil m gives nil.

func (*Message) FindAll

func (m *Message) FindAll(tag, qualifier string) []*Segment

FindAll returns every segment matching tag and qualifier.

func (*Message) FindAllGroups

func (m *Message) FindAllGroups(name string) []*SegmentGroup

FindAllGroups returns every repetition of the named top-level segment group.

func (*Message) FindGroup

func (m *Message) FindGroup(name string) *SegmentGroup

FindGroup returns the first repetition of the named top-level segment group, or nil.

func (*Message) FunctionalGroup

func (m *Message) FunctionalGroup() *FunctionalGroup

FunctionalGroup returns the message's functional group, or nil when it is not in one or m is nil.

func (*Message) Groups

func (m *Message) Groups() []*SegmentGroup

Groups returns the top-level segment groups, built by validation or AddGroup. The slice is a copy.

func (*Message) Header

func (m *Message) Header() *Segment

Header returns the UNH segment, creating an empty one if there is none. A nil m gives nil.

func (*Message) Key

func (m *Message) Key() Key

Key returns the message identifier, read from UNH rather than held apart from it. A nil m gives a zero Key.

func (*Message) Ref

func (m *Message) Ref() string

Ref returns the message reference number, which UNT repeats.

func (*Message) Segments

func (m *Message) Segments() []*Segment

Segments returns the segments between UNH and UNT in write order. The slice is a copy, and the segments are the message's own.

func (*Message) SetKey

func (m *Message) SetKey(k Key)

SetKey writes the message identifier.

func (*Message) SetRef

func (m *Message) SetRef(ref string)

SetRef writes the message reference number.

type Node

type Node struct {
	// Tag is the segment tag, for a segment node.
	Tag string
	// Group is the segment group's label, such as SG2, for a segment group
	// node.
	Group string
	// Qualifier is the value element 1 of a segment must have. Empty accepts
	// any.
	Qualifier string
	// Qualifiers lists several values element 1 may have, any of which is
	// accepted, for a segment that may carry any of them in any order. Set
	// either Qualifier or Qualifiers, not both.
	Qualifiers []string
	// Required means the segment or segment group must appear at least once.
	// For a node with Qualifiers, any one of them is enough.
	Required bool
	// Max is how many times it may repeat. Zero means once, and Unbounded
	// means without limit.
	Max int
	// Nodes is a segment group's contents, in order. A new repetition of the
	// segment group starts with any of its nodes up to and including the
	// first required one.
	Nodes []Node
}

Node is one entry in a message specification, either a segment or a segment group, never both.

func (Node) IsSegment

func (n Node) IsSegment() bool

IsSegment reports whether n names a segment rather than a segment group.

type ReadOption

type ReadOption func(*readConfig)

ReadOption adjusts how an interchange is read. See RejectReadOn, MaxSegmentSize and DefaultDelimiters.

func DefaultDelimiters

func DefaultDelimiters(d Delimiters) ReadOption

DefaultDelimiters makes Read use d for an interchange with no UNA. A UNA still declares its own. By default, an interchange with no UNA is read with the level A separators.

Example

A file without a UNA that uses the syntax level B separators reads once DefaultDelimiters names them: IS1 between components, IS3 between elements and IS4 after each segment.

package main

import (
	"errors"
	"fmt"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	levelB := strings.NewReplacer(":", "\x1f", "+", "\x1d", "'", "\x1c").Replace(
		"UNB+UNOB:2+SENDER+RECIPIENT+260101:1200+REF1'" +
			"UNH+M1+BAPLIE:D:95B:UN'BGM++BAY1+9'UNT+3+M1'UNZ+1+REF1'")

	_, err := edifact.Read(strings.NewReader(levelB))
	fmt.Println(errors.Is(err, edifact.ErrUnsupportedSeparators))

	ix, err := edifact.Read(strings.NewReader(levelB), edifact.DefaultDelimiters(edifact.Delimiters{
		Component: 0x1f,
		Element:   0x1d,
		Segment:   0x1c,
	}))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(ix.Message(1).Find("BGM", "").Element(2))
}
Output:
true
BAY1

func MaxSegmentSize

func MaxSegmentSize(n int) ReadOption

MaxSegmentSize makes Read return ErrSegmentTooLong for a segment longer than n bytes, not counting its terminator or whitespace before it. By default, the limit is 16 MiB.

Example

MaxSegmentSize bounds how long one segment may be, not counting its terminator.

package main

import (
	"errors"
	"fmt"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

// bayplan is a short interchange cut down from a BAPLIE bayplan: two
// containers in their stowage cells. Newlines between segments are optional
// and ignored on read.
const bayplan = `UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
DTM+137:202601011200:203'
LOC+147+0010204::5'
MEA+WT++KGM:12000'
LOC+9+XXAAA:139:6'
LOC+11+XXBBB:139:6'
EQD+CN+XXXU1234567+22G1'
LOC+147+0010206::5'
MEA+WT++KGM:8500.5'
LOC+9+XXAAA:139:6'
LOC+11+XXCCC:139:6'
EQD+CN+XXXU7654321+45G1'
UNT+14+M1'
UNZ+1+REF1'`

func main() {
	_, err := edifact.Read(strings.NewReader(bayplan), edifact.MaxSegmentSize(20))
	fmt.Println(errors.Is(err, edifact.ErrSegmentTooLong), err)
}
Output:
true edifact: segment is too long: over 20 bytes

func RejectReadOn

func RejectReadOn(s Severity) ReadOption

RejectReadOn makes Read return the first diagnostic of severity s or above as an error. By default, Read rejects none.

Example

Without RejectReadOn, Read collects diagnostics on the interchange. With it, Read returns the first one at or above the severity as an error.

package main

import (
	"fmt"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

// bayplan is a short interchange cut down from a BAPLIE bayplan: two
// containers in their stowage cells. Newlines between segments are optional
// and ignored on read.
const bayplan = `UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
DTM+137:202601011200:203'
LOC+147+0010204::5'
MEA+WT++KGM:12000'
LOC+9+XXAAA:139:6'
LOC+11+XXBBB:139:6'
EQD+CN+XXXU1234567+22G1'
LOC+147+0010206::5'
MEA+WT++KGM:8500.5'
LOC+9+XXAAA:139:6'
LOC+11+XXCCC:139:6'
EQD+CN+XXXU7654321+45G1'
UNT+14+M1'
UNZ+1+REF1'`

func main() {
	miscounted := strings.Replace(bayplan, "UNT+14+M1'", "UNT+15+M1'", 1)

	ix, err := edifact.Read(strings.NewReader(miscounted))
	fmt.Println(len(ix.Diagnostics), "diagnostics,", err)

	_, err = edifact.Read(strings.NewReader(miscounted), edifact.RejectReadOn(edifact.Error))
	fmt.Println(err)
}
Output:
1 diagnostics, <nil>
edifact: error: message 1 (M1), segment 14, UNT: count is 15, counted 14

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry resolves a Key to a Spec. Each Registry is independent of every other, and the package holds none of its own.

Register and Load are not safe for concurrent use. Once a Registry is built, any number of goroutines may read, validate and write through it.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty Registry.

func (*Registry) Len

func (r *Registry) Len() int

Len reports how many specs are registered.

func (*Registry) Load

func (r *Registry) Load(specs []Spec) error

Load registers each spec in turn, stopping at the first that fails.

Example

Load registers several specs at once. When more than one matches a message, the one constraining the most Key fields is used.

package main

import (
	"fmt"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	plain := edifact.Spec{
		Key: edifact.Key{
			Type:    edifact.Is("BAPLIE"),
			Version: edifact.Is("D"),
			Release: edifact.Is("95B"),
			Agency:  edifact.Is("UN"),
		},
		Nodes: []edifact.Node{
			{Tag: "BGM", Required: true},
			{Tag: "DTM", Qualifier: "137", Required: true},
		},
	}

	// The same message with an association code, as an industry guide might
	// define it.
	variant := plain.Clone()
	variant.Key.Association = edifact.Is("XX01")
	variant.Nodes = append(variant.Nodes, edifact.Node{Tag: "FTX", Qualifier: "AAA"})

	reg := edifact.NewRegistry()
	if err := reg.Load([]edifact.Spec{plain, variant}); err != nil {
		fmt.Println(err)
		return
	}

	src := "UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'" +
		"UNH+M1+BAPLIE:D:95B:UN'BGM++BAY1+9'DTM+137:202601011200:203'UNT+4+M1'" +
		"UNH+M2+BAPLIE:D:95B:UN:XX01'BGM++BAY2+9'DTM+137:202601011200:203'FTX+AAA+++NOTE'UNT+5+M2'" +
		"UNZ+2+REF1'"
	ix, err := reg.Read(strings.NewReader(src))
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(len(ix.Diagnostics), "diagnostics")
	for _, m := range ix.Messages() {
		spec, _ := reg.Lookup(m.Key())
		fmt.Println(m.Ref(), m.Key(), "uses", spec.Key)
	}
}
Output:
0 diagnostics
M1 BAPLIE:D:95B:UN:- uses BAPLIE:D:95B:UN:*
M2 BAPLIE:D:95B:UN:XX01 uses BAPLIE:D:95B:UN:XX01

func (*Registry) Lookup

func (r *Registry) Lookup(k Key) (*Spec, bool)

Lookup returns a copy of the spec matching k. Where several match, the one constraining the most fields wins, and the latest registration breaks a tie.

func (*Registry) Read

func (r *Registry) Read(src io.Reader, opts ...ReadOption) (*Interchange, error)

Read parses an interchange, checking each message against the specs in r. A nil r consults no spec.

Example

Reading against a specification sorts the segments into segment groups, so each container is read together with its own cell, weight and ports.

package main

import (
	"fmt"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

// bayplan is a short interchange cut down from a BAPLIE bayplan: two
// containers in their stowage cells. Newlines between segments are optional
// and ignored on read.
const bayplan = `UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
DTM+137:202601011200:203'
LOC+147+0010204::5'
MEA+WT++KGM:12000'
LOC+9+XXAAA:139:6'
LOC+11+XXBBB:139:6'
EQD+CN+XXXU1234567+22G1'
LOC+147+0010206::5'
MEA+WT++KGM:8500.5'
LOC+9+XXAAA:139:6'
LOC+11+XXCCC:139:6'
EQD+CN+XXXU7654321+45G1'
UNT+14+M1'
UNZ+1+REF1'`

// bayplanSpec is a simplified specification after the BAPLIE D.95B segment
// table, which has more segments than this: a header, then one SG2 per stowage
// cell, each holding an SG3 for the container in it.
func bayplanSpec() edifact.Spec {
	return edifact.Spec{
		Key: edifact.Key{
			Type:    edifact.Is("BAPLIE"),
			Version: edifact.Is("D"),
			Release: edifact.Is("95B"),
			Agency:  edifact.Is("UN"),
		},
		Nodes: []edifact.Node{
			{Tag: "BGM", Required: true},
			{Tag: "DTM", Qualifier: "137", Required: true},
			{Group: "SG2", Max: edifact.Unbounded, Nodes: []edifact.Node{
				{Tag: "LOC", Qualifier: "147", Required: true},
				{Tag: "MEA", Qualifier: "WT"},
				{Tag: "LOC", Qualifiers: []string{"9", "11"}, Max: 9},
				{Group: "SG3", Max: 3, Nodes: []edifact.Node{
					{Tag: "EQD", Qualifier: "CN", Required: true},
				}},
			}},
		},
	}
}

func main() {
	reg := edifact.NewRegistry()
	if err := reg.Register(bayplanSpec()); err != nil {
		fmt.Println(err)
		return
	}

	ix, err := reg.Read(strings.NewReader(bayplan))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(len(ix.Diagnostics), "diagnostics")

	for _, cell := range ix.Message(1).FindAllGroups("SG2") {
		weight, _ := cell.Find("MEA", "WT").Float(3, 2)
		container := cell.FindGroup("SG3").Find("EQD", "CN").Element(2)
		fmt.Printf("%s in %s, %.1f kg, %s to %s\n",
			container,
			cell.Find("LOC", "147").Component(2, 1),
			weight,
			cell.Find("LOC", "9").Component(2, 1),
			cell.Find("LOC", "11").Component(2, 1))
	}
}
Output:
0 diagnostics
XXXU1234567 in 0010204, 12000.0 kg, XXAAA to XXBBB
XXXU7654321 in 0010206, 8500.5 kg, XXAAA to XXCCC

func (*Registry) Register

func (r *Registry) Register(s Spec) error

Register validates s and stores a deep copy, so later edits to s do not reach the Registry. It refuses a malformed spec, such as one with no message type, a node naming both or neither of a tag and a segment group, or a segment declared twice in a row.

Registering a Key already present overrides the earlier spec for it, since Registry.Lookup prefers the later registration.

func (*Registry) Validate

func (r *Registry) Validate(ix *Interchange) []Diagnostic

Validate checks every message in ix against its registered spec, and fills in each message's segment groups. A message whose Key resolves to no spec is reported and left ungrouped.

Example

Validate checks an interchange built in code against the registry's specs, before Write would refuse it.

package main

import (
	"fmt"

	"github.com/MaldivesPorts/edifact"
)

// bayplanSpec is a simplified specification after the BAPLIE D.95B segment
// table, which has more segments than this: a header, then one SG2 per stowage
// cell, each holding an SG3 for the container in it.
func bayplanSpec() edifact.Spec {
	return edifact.Spec{
		Key: edifact.Key{
			Type:    edifact.Is("BAPLIE"),
			Version: edifact.Is("D"),
			Release: edifact.Is("95B"),
			Agency:  edifact.Is("UN"),
		},
		Nodes: []edifact.Node{
			{Tag: "BGM", Required: true},
			{Tag: "DTM", Qualifier: "137", Required: true},
			{Group: "SG2", Max: edifact.Unbounded, Nodes: []edifact.Node{
				{Tag: "LOC", Qualifier: "147", Required: true},
				{Tag: "MEA", Qualifier: "WT"},
				{Tag: "LOC", Qualifiers: []string{"9", "11"}, Max: 9},
				{Group: "SG3", Max: 3, Nodes: []edifact.Node{
					{Tag: "EQD", Qualifier: "CN", Required: true},
				}},
			}},
		},
	}
}

func main() {
	reg := edifact.NewRegistry()
	if err := reg.Register(bayplanSpec()); err != nil {
		fmt.Println(err)
		return
	}

	ix := edifact.NewInterchange()
	m := ix.AddMessage(edifact.Key{
		Type:    edifact.Is("BAPLIE"),
		Version: edifact.Is("D"),
		Release: edifact.Is("95B"),
		Agency:  edifact.Is("UN"),
	})
	m.SetRef("M1")
	m.Add("BGM", "", "BAY1", "9")
	m.AddComposite("LOC", []string{"147"}, []string{"0010204", "", "5"})

	for _, d := range reg.Validate(ix) {
		fmt.Println(d)
	}
}
Output:
error: message 1 (M1), segment 3, DTM+137: required segment is missing

func (*Registry) Write

func (r *Registry) Write(w io.Writer, ix *Interchange, opts ...WriteOption) error

Write validates ix against the specs in r and then serialises it, stopping at the first structural error, or the first warning under RejectWriteOn(Warning). A nil r skips validation.

type Segment

type Segment struct {
	// Tag is the segment code, without any explicit nesting and repetition
	// indices.
	Tag string
	// contains filtered or unexported fields
}

Segment is one tagged line.

func (*Segment) Component

func (s *Segment) Component(i, j int) string

Component returns component j of the first repetition of element i, both counting from 1, or "" when absent.

func (*Segment) Components

func (s *Segment) Components(i int) []string

Components returns a copy of the components of the first repetition of element i, counting from 1, or nil when absent.

func (*Segment) Element

func (s *Segment) Element(i int) string

Element returns component 1 of the first repetition of element i, counting from 1 as the published tables do, or "" when absent.

func (*Segment) Float

func (s *Segment) Float(elem, comp int) (float64, error)

Float reads component comp of element elem, both counting from 1, as a number. Either a dot or a comma is accepted as the decimal mark, whatever the interchange declares.

Example

Float accepts a dot or a comma as the decimal mark, and rejects anything ISO 9735 does not allow as a number.

package main

import (
	"errors"
	"fmt"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	ix := edifact.NewInterchange()
	m := ix.AddMessage(edifact.Key{})
	mea := m.AddComposite("MEA", []string{"WT"}, nil, []string{"KGM", "8500,5"})

	v, err := mea.Float(3, 2)
	fmt.Println(v, err)

	mea.Set(3, 2, "1,000.5")
	_, err = mea.Float(3, 2)
	fmt.Println(errors.Is(err, edifact.ErrNumeric))
}
Output:
8500.5 <nil>
true

func (*Segment) Index

func (s *Segment) Index() []string

Index returns a copy of the explicit nesting and repetition indices after the segment code, or nil when the tag carries none.

Example

A tag can carry its nesting level and repetition number as components. Specs match the plain tag, and Index returns the numbers.

package main

import (
	"fmt"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	src := "UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'" +
		"UNH+M1+TSTMSG:D:95B:UN'DDD:1:2+VALUE'UNT+3+M1'UNZ+1+REF1'"
	ix, err := edifact.Read(strings.NewReader(src))
	if err != nil {
		fmt.Println(err)
		return
	}
	s := ix.Message(1).Find("DDD", "")
	fmt.Println(s.Tag, s.Index(), s.Element(1))
}
Output:
DDD [1 2] VALUE

func (*Segment) Int

func (s *Segment) Int(elem, comp int) (int, error)

Int reads component comp of element elem, both counting from 1, as a whole number: an optional minus sign and digits. Leading zeros are accepted.

func (*Segment) Len

func (s *Segment) Len() int

Len returns the number of elements.

func (*Segment) Qualifier

func (s *Segment) Qualifier() string

Qualifier returns element 1, the value that distinguishes segments sharing a tag.

func (*Segment) Repetition

func (s *Segment) Repetition(i, r int) []string

Repetition returns a copy of the components of repetition r of element i, both counting from 1, or nil when absent.

Example

In syntax version 4 an element can repeat within one segment, separated by the character in the fifth position of the UNA.

package main

import (
	"fmt"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	src := "UNA:+.?*'" +
		"UNB+UNOA:4+SENDER+RECIPIENT+20260101:1200+REF1'" +
		"UNH+M1+TSTMSG:D:95B:UN'" +
		"COM+111:TE*222:FX'" +
		"UNT+3+M1'UNZ+1+REF1'"
	ix, err := edifact.Read(strings.NewReader(src))
	if err != nil {
		fmt.Println(err)
		return
	}
	com := ix.Message(1).Find("COM", "")

	for r := 1; r <= com.Repetitions(1); r++ {
		fmt.Println(com.Repetition(1, r))
	}
	fmt.Println(com.Element(1))
}
Output:
[111 TE]
[222 FX]
111

func (*Segment) Repetitions

func (s *Segment) Repetitions(i int) int

Repetitions returns the number of repetitions of element i, counting from 1, or 0 when the element is empty or absent.

func (*Segment) Set

func (s *Segment) Set(i, j int, v string)

Set writes component j of the first repetition of element i, both counting from 1, extending the segment as needed.

func (*Segment) SetFloat

func (s *Segment) SetFloat(elem, comp int, v float64, prec int) error

SetFloat writes v to component comp of element elem, both counting from 1. It is held with a dot as the decimal mark, and Write renders it with the mark the interchange declares. prec is the number of decimal places, or -1 for the fewest digits that parse back to v. An infinite or NaN v gives ErrNumeric and writes nothing.

Example

A file without a UNA may use a comma as the decimal mark. Write declares a dot and writes text values as they are, so a number read as text keeps its comma until it is set again with SetFloat.

package main

import (
	"fmt"
	"os"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	src := "UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'" +
		"UNH+M1+BAPLIE:D:95B:UN'MEA+WT++KGM:8500,5'UNT+3+M1'UNZ+1+REF1'"
	ix, err := edifact.Read(strings.NewReader(src))
	if err != nil {
		fmt.Println(err)
		return
	}
	mea := ix.Message(1).Find("MEA", "WT")

	v, err := mea.Float(3, 2)
	if err != nil {
		fmt.Println(err)
		return
	}
	if err := mea.SetFloat(3, 2, v, -1); err != nil {
		fmt.Println(err)
		return
	}
	if err := edifact.Write(os.Stdout, ix, edifact.SegmentSuffixLF()); err != nil {
		fmt.Println(err)
	}
}
Output:
UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
MEA+WT++KGM:8500.5'
UNT+3+M1'
UNZ+1+REF1'

func (*Segment) SetIndex

func (s *Segment) SetIndex(index ...string)

SetIndex replaces the explicit indices written after the segment code. No arguments removes them.

func (*Segment) SetInt

func (s *Segment) SetInt(elem, comp int, v int)

SetInt writes v to component comp of element elem, both counting from 1.

func (*Segment) SetRepetition

func (s *Segment) SetRepetition(i, r, j int, v string)

SetRepetition writes component j of repetition r of element i, all counting from 1, extending the segment as needed.

func (*Segment) SetTime

func (s *Segment) SetTime(elem, comp int, t time.Time, format string) error

SetTime writes t in the given format to component comp of element elem, both counting from 1, and the format qualifier to the component after it. For formats 301 and 404, whose zone is written as letters, it converts t to UTC and writes UTC. For other formats it writes t's own clock, and for 205 its offset as well.

func (*Segment) Time

func (s *Segment) Time(elem, comp int) (time.Time, error)

Time reads component comp of element elem, both counting from 1, as a date and time, taking its format qualifier from the component after it. That is the layout of composite C507, where a DTM carries the qualifier, the value and the format in one element.

Example

Element and Component count from 1, the way the published segment tables number positions. Typed accessors convert one component.

package main

import (
	"fmt"
	"strings"
	"time"

	"github.com/MaldivesPorts/edifact"
)

// bayplan is a short interchange cut down from a BAPLIE bayplan: two
// containers in their stowage cells. Newlines between segments are optional
// and ignored on read.
const bayplan = `UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
DTM+137:202601011200:203'
LOC+147+0010204::5'
MEA+WT++KGM:12000'
LOC+9+XXAAA:139:6'
LOC+11+XXBBB:139:6'
EQD+CN+XXXU1234567+22G1'
LOC+147+0010206::5'
MEA+WT++KGM:8500.5'
LOC+9+XXAAA:139:6'
LOC+11+XXCCC:139:6'
EQD+CN+XXXU7654321+45G1'
UNT+14+M1'
UNZ+1+REF1'`

func main() {
	ix, err := edifact.Read(strings.NewReader(bayplan))
	if err != nil {
		fmt.Println(err)
		return
	}
	dtm := ix.Message(1).Find("DTM", "137")

	fmt.Println(dtm.Component(1, 2), "in format", dtm.Component(1, 3))
	t, err := dtm.Time(1, 2)
	fmt.Println(t.Format(time.RFC3339), err)
}
Output:
202601011200 in format 203
2026-01-01T12:00:00Z <nil>

func (*Segment) TimeIn

func (s *Segment) TimeIn(elem, comp int, format string) (time.Time, error)

TimeIn reads component comp of element elem, both counting from 1, as a date and time in the given format, for segments that do not carry the format qualifier beside the value.

func (*Segment) TimeRange

func (s *Segment) TimeRange(elem, comp int) (start, end time.Time, err error)

TimeRange reads component comp of element elem, both counting from 1, as a period, taking its format qualifier from the component after it.

type SegmentGroup

type SegmentGroup struct {
	// Name is the segment group's label in the spec, such as SG2.
	Name string
	// contains filtered or unexported fields
}

SegmentGroup is one repetition of a segment group declared by a spec, such as SG2. Its segments are pointers to the message's own, not copies, so the message stays the single storage and carries write order.

func (*SegmentGroup) Add

func (g *SegmentGroup) Add(tag string, elems ...string) *Segment

Add builds a segment from elements, one component each, places it after the segment group's last segment, both in the segment group and in the message, and returns it. A segment group with no segments yet places it after the previous repetition of the same segment group, or else after the nearest earlier segment group beside it, or else before the nearest later one, or else after its container's own segments.

Example

Add on a segment group places the new segment inside that segment group, even when it is not the last one, so the written order matches the segment group.

package main

import (
	"fmt"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

// bayplan is a short interchange cut down from a BAPLIE bayplan: two
// containers in their stowage cells. Newlines between segments are optional
// and ignored on read.
const bayplan = `UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
DTM+137:202601011200:203'
LOC+147+0010204::5'
MEA+WT++KGM:12000'
LOC+9+XXAAA:139:6'
LOC+11+XXBBB:139:6'
EQD+CN+XXXU1234567+22G1'
LOC+147+0010206::5'
MEA+WT++KGM:8500.5'
LOC+9+XXAAA:139:6'
LOC+11+XXCCC:139:6'
EQD+CN+XXXU7654321+45G1'
UNT+14+M1'
UNZ+1+REF1'`

// bayplanSpec is a simplified specification after the BAPLIE D.95B segment
// table, which has more segments than this: a header, then one SG2 per stowage
// cell, each holding an SG3 for the container in it.
func bayplanSpec() edifact.Spec {
	return edifact.Spec{
		Key: edifact.Key{
			Type:    edifact.Is("BAPLIE"),
			Version: edifact.Is("D"),
			Release: edifact.Is("95B"),
			Agency:  edifact.Is("UN"),
		},
		Nodes: []edifact.Node{
			{Tag: "BGM", Required: true},
			{Tag: "DTM", Qualifier: "137", Required: true},
			{Group: "SG2", Max: edifact.Unbounded, Nodes: []edifact.Node{
				{Tag: "LOC", Qualifier: "147", Required: true},
				{Tag: "MEA", Qualifier: "WT"},
				{Tag: "LOC", Qualifiers: []string{"9", "11"}, Max: 9},
				{Group: "SG3", Max: 3, Nodes: []edifact.Node{
					{Tag: "EQD", Qualifier: "CN", Required: true},
				}},
			}},
		},
	}
}

func main() {
	reg := edifact.NewRegistry()
	spec := bayplanSpec()
	spec.Nodes[2].Nodes = append(spec.Nodes[2].Nodes, edifact.Node{Tag: "FTX", Qualifier: "AAA"})
	if err := reg.Register(spec); err != nil {
		fmt.Println(err)
		return
	}
	ix, err := reg.Read(strings.NewReader(bayplan))
	if err != nil {
		fmt.Println(err)
		return
	}

	first := ix.Message(1).FindGroup("SG2")
	first.Add("FTX", "AAA", "", "", "HANDLE WITH CARE")

	for _, s := range ix.Message(1).Segments() {
		fmt.Println(strings.TrimSpace(s.Tag + " " + s.Qualifier()))
	}
}
Output:
BGM
DTM 137
LOC 147
MEA WT
LOC 9
LOC 11
EQD CN
FTX AAA
LOC 147
MEA WT
LOC 9
LOC 11
EQD CN

func (*SegmentGroup) AddComposite

func (g *SegmentGroup) AddComposite(tag string, elems ...[]string) *Segment

AddComposite is Add for a segment whose elements each carry several components. The segment holds copies of elems.

func (*SegmentGroup) AddGroup

func (g *SegmentGroup) AddGroup(name string) *SegmentGroup

AddGroup appends a nested segment group.

func (*SegmentGroup) Find

func (g *SegmentGroup) Find(tag, qualifier string) *Segment

Find returns the first segment in the segment group matching tag and qualifier.

func (*SegmentGroup) FindAll

func (g *SegmentGroup) FindAll(tag, qualifier string) []*Segment

FindAll returns every segment in the segment group matching tag and qualifier.

func (*SegmentGroup) FindAllGroups

func (g *SegmentGroup) FindAllGroups(name string) []*SegmentGroup

FindAllGroups returns every repetition of the named nested segment group.

func (*SegmentGroup) FindGroup

func (g *SegmentGroup) FindGroup(name string) *SegmentGroup

FindGroup returns the first repetition of the named nested segment group, or nil.

func (*SegmentGroup) Groups

func (g *SegmentGroup) Groups() []*SegmentGroup

Groups returns the nested segment groups. The slice is a copy.

func (*SegmentGroup) Segments

func (g *SegmentGroup) Segments() []*Segment

Segments returns the segment group's segments in message order. The slice is a copy, and the segments are the message's own.

type Severity

type Severity int

Severity ranks a diagnostic as a warning or an error.

const (
	// Warning marks a problem that leaves the interchange usable, such as a
	// segment a spec does not declare.
	Warning Severity = iota
	// Error marks a problem that breaks the standard or a spec, such as a
	// missing required segment. Write with a registry refuses a message whose
	// validation gives one.
	Error
)

func (Severity) String

func (s Severity) String() string

String returns "warning" or "error".

type Spec

type Spec struct {
	// Key selects the messages this spec applies to.
	Key Key
	// Nodes are the message's segments and segment groups, in order.
	Nodes []Node
}

Spec describes the segments between UNH and UNT for one message dialect.

func (Spec) Clone

func (s Spec) Clone() Spec

Clone returns a deep copy. Key holds pointers and Nodes is a slice, so a shallow copy would let an edit reach the original.

Example

Clone copies a spec deeply, so a variant can change its key and nodes without touching the original.

package main

import (
	"fmt"

	"github.com/MaldivesPorts/edifact"
)

// bayplanSpec is a simplified specification after the BAPLIE D.95B segment
// table, which has more segments than this: a header, then one SG2 per stowage
// cell, each holding an SG3 for the container in it.
func bayplanSpec() edifact.Spec {
	return edifact.Spec{
		Key: edifact.Key{
			Type:    edifact.Is("BAPLIE"),
			Version: edifact.Is("D"),
			Release: edifact.Is("95B"),
			Agency:  edifact.Is("UN"),
		},
		Nodes: []edifact.Node{
			{Tag: "BGM", Required: true},
			{Tag: "DTM", Qualifier: "137", Required: true},
			{Group: "SG2", Max: edifact.Unbounded, Nodes: []edifact.Node{
				{Tag: "LOC", Qualifier: "147", Required: true},
				{Tag: "MEA", Qualifier: "WT"},
				{Tag: "LOC", Qualifiers: []string{"9", "11"}, Max: 9},
				{Group: "SG3", Max: 3, Nodes: []edifact.Node{
					{Tag: "EQD", Qualifier: "CN", Required: true},
				}},
			}},
		},
	}
}

func main() {
	base := bayplanSpec()

	variant := base.Clone()
	variant.Key.Association = edifact.Is("XX01")
	unit := &variant.Nodes[2]
	unit.Nodes = append(unit.Nodes, edifact.Node{Tag: "FTX", Qualifier: "AAA"})

	fmt.Println(base.Key, len(base.Nodes[2].Nodes), "nodes in SG2")
	fmt.Println(variant.Key, len(variant.Nodes[2].Nodes), "nodes in SG2")
}
Output:
BAPLIE:D:95B:UN:* 4 nodes in SG2
BAPLIE:D:95B:UN:XX01 5 nodes in SG2

type WriteOption

type WriteOption func(*writeConfig)

WriteOption adjusts how an interchange is serialised. See SegmentSuffixLF, SegmentSuffixCRLF, AllowIncompleteHeaders and RejectWriteOn.

func AllowIncompleteHeaders

func AllowIncompleteHeaders() WriteOption

AllowIncompleteHeaders writes a UNB, UNG or UNH that lacks elements ISO 9735 makes mandatory, rather than returning ErrIncompleteHeader.

func RejectWriteOn

func RejectWriteOn(s Severity) WriteOption

RejectWriteOn makes Write return the first diagnostic of severity s or above as an error. By default, Write rejects errors. A severity above Error counts as Error.

Example

A registry's Write refuses errors by default. RejectWriteOn(Warning) makes it refuse warnings too, such as a segment the spec does not declare. A segment added at the end of the message falls inside its last segment group.

package main

import (
	"fmt"
	"io"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

// bayplan is a short interchange cut down from a BAPLIE bayplan: two
// containers in their stowage cells. Newlines between segments are optional
// and ignored on read.
const bayplan = `UNA:+.? '
UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
DTM+137:202601011200:203'
LOC+147+0010204::5'
MEA+WT++KGM:12000'
LOC+9+XXAAA:139:6'
LOC+11+XXBBB:139:6'
EQD+CN+XXXU1234567+22G1'
LOC+147+0010206::5'
MEA+WT++KGM:8500.5'
LOC+9+XXAAA:139:6'
LOC+11+XXCCC:139:6'
EQD+CN+XXXU7654321+45G1'
UNT+14+M1'
UNZ+1+REF1'`

// bayplanSpec is a simplified specification after the BAPLIE D.95B segment
// table, which has more segments than this: a header, then one SG2 per stowage
// cell, each holding an SG3 for the container in it.
func bayplanSpec() edifact.Spec {
	return edifact.Spec{
		Key: edifact.Key{
			Type:    edifact.Is("BAPLIE"),
			Version: edifact.Is("D"),
			Release: edifact.Is("95B"),
			Agency:  edifact.Is("UN"),
		},
		Nodes: []edifact.Node{
			{Tag: "BGM", Required: true},
			{Tag: "DTM", Qualifier: "137", Required: true},
			{Group: "SG2", Max: edifact.Unbounded, Nodes: []edifact.Node{
				{Tag: "LOC", Qualifier: "147", Required: true},
				{Tag: "MEA", Qualifier: "WT"},
				{Tag: "LOC", Qualifiers: []string{"9", "11"}, Max: 9},
				{Group: "SG3", Max: 3, Nodes: []edifact.Node{
					{Tag: "EQD", Qualifier: "CN", Required: true},
				}},
			}},
		},
	}
}

func main() {
	reg := edifact.NewRegistry()
	if err := reg.Register(bayplanSpec()); err != nil {
		fmt.Println(err)
		return
	}
	ix, err := edifact.Read(strings.NewReader(bayplan))
	if err != nil {
		fmt.Println(err)
		return
	}
	ix.Message(1).Add("FTX", "AAI", "", "", "NOTE")

	fmt.Println(reg.Write(io.Discard, ix))
	fmt.Println(reg.Write(io.Discard, ix, edifact.RejectWriteOn(edifact.Warning)))
}
Output:
<nil>
edifact: warning: message 1 (M1), segment 14, /SG2/FTX+AAI: segment is not declared here

func SegmentSuffixCRLF

func SegmentSuffixCRLF() WriteOption

SegmentSuffixCRLF writes a carriage return and a line feed after every segment terminator. Without a suffix option, segments run together.

func SegmentSuffixLF

func SegmentSuffixLF() WriteOption

SegmentSuffixLF writes a line feed after every segment terminator. Without a suffix option, segments run together.

Jump to

Keyboard shortcuts

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