edifact

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 11 Imported by: 0

README

edifact

A Go library for reading and writing UN/EDIFACT interchanges. It needs Go 1.22 or later.

go get github.com/MaldivesPorts/edifact

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.SetDate("260101")
ix.SetTime("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.
  • CHANGELOG.md lists what changed in each version.

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. Only UNOA, UNOB, UNOC and UNOW are checked and converted. Other syntax levels, such as UNOD to UNOK (other parts of ISO 8859) and UNOX and UNOY (ISO 2022 code extension), are read with a warning and written with their values as they are.
  • Level B separators without a UNA. A file with no UNA that uses the level B default separators (IS3 between elements, IS1 between components, IS4 after each segment) is not detected, and needs DefaultDelimiters. Detecting them automatically would fix this.
  • 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 are limited. Write refuses empty mandatory header elements and a date of preparation of the wrong length for the syntax version, but checks nothing else about their format, such as a month of 13.
  • 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 formats are supported, each a DateFormat constant:

2    FormatDDMMYY               301  FormatYYMMDDHHMMZZZ
3    FormatMMDDYY               401  FormatHHMM
101  FormatYYMMDD               402  FormatHHMMSS
102  FormatCCYYMMDD             404  FormatHHMMSSZZZ
201  FormatYYMMDDHHMM           602  FormatCCYY
202  FormatYYMMDDHHMMSS         610  FormatCCYYMM
203  FormatCCYYMMDDHHMM         718  FormatCCYYMMDDPeriod
204  FormatCCYYMMDDHHMMSS       719  FormatCCYYMMDDHHMMPeriod
205  FormatCCYYMMDDHHMMZHHMM

A qualifier read from a message converts with DateFormat(code). Any other gives ErrDateFormat.

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 formats that name a period.

Reading many values

Fields reads values the way the typed accessors do, but records a warning for each value that will not convert instead of returning an error. A caller can read every value it needs and handle the problems once:

f := edifact.NewFields(m)
date := f.Time(m.Find("DTM", "137"), 1, 2, "document date")
for _, cell := range m.FindAllGroups("SG2") {
	weight := f.Float(cell.Find("MEA", "WT"), 3, 2, "weight")
	// ...
}
for _, d := range f.Diagnostics {
	log.Println(d)
}

An absent segment or an empty position gives the zero value and records nothing. Each warning is a Diagnostic naming the message, the segment and its segment groups, followed by the caller's label for the value. Its Err holds the conversion error, so errors can be compared with errors.Is later. Fields.Addf records a problem only the caller can see, such as one value missing where another makes it necessary.

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
  • a value holding a character outside the interchange's charset, once per segment, and a charset this package does not support
  • every problem a specification finds

Each Diagnostic gives its Severity, the message it concerns and the segment. Fields produces diagnostics of the same type for values that will not convert.

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 in UNOA, syntax version 2. Set the UNB values with its setters, then add messages with Interchange.AddMessage. Interchange.SetDateTime writes the date and time of preparation from a time.Time, in the format the syntax version requires: CCYYMMDD in version 4 and YYMMDD below it. Interchange.SetDate and Interchange.SetTime write them as text. 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.

SetTime writes the time's own clock, and its offset when the format carries one. A format whose zone is written as letters gets the time in UTC.

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
  • checks every value against the interchange's charset, and encodes UNOC values as ISO 8859-1. A character outside the charset gives ErrCharset, naming its position.
  • 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
  • returns ErrHeaderDate for a UNB or UNG date whose length does not match the syntax version, and ErrSyntaxVersion for UNOW below version 4
  • 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 into and writing from types

A type that implements Unmarshaler reads itself from a message, and one that implements Marshaler writes itself into one. A type may implement either or both. Its methods choose every segment, code and format, so the mapping stays in the caller's code:

func (h *Header) UnmarshalEDIFACT(m *edifact.Message, f *edifact.Fields) error {
	h.Number = m.Find("BGM", "").Element(2)
	h.Date = f.Time(m.Find("DTM", "137"), 1, 2, "document date")
	return nil
}

func (h Header) MarshalEDIFACT(m *edifact.Message) error {
	m.Add("BGM", "", h.Number, "9")
	return m.Add("DTM", "137").SetTime(1, 2, h.Date, edifact.FormatCCYYMMDDHHMM)
}

Unmarshal calls UnmarshalEDIFACT with a Fields located in the message and returns the diagnostics it collected. Marshal calls MarshalEDIFACT. Write with a registry then checks the result against its spec.

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, which is mostly a character set, a Charset. 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.

Charsets

A Charset is one of the syntax levels this package reads and writes:

UNOA  level A: upper case A to Z, digits, space and . , - ( ) / = ' + : ?
      ! " % & * ; < >
UNOB  level B: level A and lower case a to z
UNOC  level C: ISO 8859-1
UNOW  level W: UTF-8, syntax version 4 only

Interchange.SetCharset and Interchange.Charset write and read it. Values in a Segment are always Go strings in UTF-8. Read decodes UNOC from ISO 8859-1 and Write encodes it back. Write refuses a value holding a character outside the charset, and Read reports one as a warning. The level A service characters ' + : ? may appear in values, and Write releases them where they clash with the separators.

With FitCharset, Write first converts what it can: an accented Latin-1 letter becomes its base letter, such as é to e, Æ to AE and ß to ss, and in UNOA lower case becomes upper case. A file naming another syntax level, such as UNOD, is read with a warning and written with its values as they are.

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.SetSyntaxVersion with 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 (
	// UNOA is level A: upper case letters, digits, space and some punctuation.
	UNOA = Charset{"UNOA"}
	// UNOB is level B: level A and lower case letters.
	UNOB = Charset{"UNOB"}
	// UNOC is level C, the ISO 8859-1 character set.
	UNOC = Charset{"UNOC"}
	// UNOW is level W, UTF-8. It needs syntax version 4.
	UNOW = Charset{"UNOW"}
)
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 ErrCharset = errors.New("edifact: character outside the charset")

ErrCharset reports a value holding a character outside the interchange's charset.

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 ErrHeaderDate = errors.New("edifact: date of preparation does not match the syntax version")

ErrHeaderDate reports a UNB or UNG date of preparation whose length does not match the syntax version: eight digits in version 4 and six below it.

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 ErrNoMessage = errors.New("edifact: no message")

ErrNoMessage reports a nil message passed to Unmarshal or Marshal.

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 ErrSyntaxVersion = errors.New("edifact: syntax version must be 1 to 4")

ErrSyntaxVersion reports a syntax version other than 1 to 4.

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 Marshal added in v0.3.0

func Marshal(m *Message, v Marshaler) error

Marshal writes v into m, usually a message just returned by Interchange.AddMessage. A nil m gives ErrNoMessage.

Example

Marshal writes a type into a message.

package main

import (
	"fmt"
	"os"
	"time"

	"github.com/MaldivesPorts/edifact"
)

// outHeader is the document number and date of a message, from BGM and DTM+137.
type outHeader struct {
	Number string
	Date   time.Time
}

// MarshalEDIFACT writes h, with its date as CCYYMMDDHHMM.
func (h outHeader) MarshalEDIFACT(m *edifact.Message) error {
	m.Add("BGM", "", h.Number, "9")
	return m.Add("DTM", "137").SetTime(1, 2, h.Date, edifact.FormatCCYYMMDDHHMM)
}

// Marshal writes a type into a message.
func main() {
	ix := edifact.NewInterchange()
	ix.SetSender("SENDER", "")
	ix.SetRecipient("RECIPIENT", "")
	ix.SetDateTime(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
	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")

	h := outHeader{Number: "BAY1", Date: time.Date(2026, 1, 2, 15, 4, 0, 0, time.UTC)}
	if err := edifact.Marshal(m, h); 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'
BGM++BAY1+9'
DTM+137:202601021504:203'
UNT+4+M1'
UNZ+1+REF1'

func ParseTime

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

ParseTime decodes a value against a DateFormat. 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 string, format DateFormat) (start, end time.Time, err error)

ParseTimeRange decodes a value against a format naming a period, such as FormatCCYYMMDDPeriod. 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.SetDate("260101")
	ix.SetTime("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 Charset added in v0.2.0

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

Charset is a syntax level this package reads and writes, named by the syntax identifier in UNB. Only the package's own values exist.

func (Charset) String added in v0.2.0

func (c Charset) String() string

String returns the syntax identifier, such as "UNOA", or "" for the zero Charset.

type DateFormat added in v0.3.0

type DateFormat string

DateFormat is a date or time format qualifier from code list 2379, such as 203 for CCYYMMDDHHMM. The constants are the formats the package decodes, named after their layouts. A qualifier read from a message converts with DateFormat(code), and one the package does not decode gives ErrDateFormat.

Example

A format qualifier read from a message converts to a DateFormat. One the package does not decode gives ErrDateFormat.

package main

import (
	"errors"
	"fmt"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	for _, code := range []string{"102", "999"} {
		t, err := edifact.ParseTime("20260102", edifact.DateFormat(code))
		fmt.Println(code, t.Format("2006-01-02"), errors.Is(err, edifact.ErrDateFormat))
	}
	fmt.Println(edifact.FormatCCYYMMDD == "102")
}
Output:
102 2026-01-02 false
999 0001-01-01 true
true
const (
	FormatDDMMYY             DateFormat = "2"
	FormatMMDDYY             DateFormat = "3"
	FormatYYMMDD             DateFormat = "101"
	FormatCCYYMMDD           DateFormat = "102"
	FormatYYMMDDHHMM         DateFormat = "201"
	FormatYYMMDDHHMMSS       DateFormat = "202"
	FormatCCYYMMDDHHMM       DateFormat = "203"
	FormatCCYYMMDDHHMMSS     DateFormat = "204"
	FormatCCYYMMDDHHMMZHHMM  DateFormat = "205"
	FormatYYMMDDHHMMZZZ      DateFormat = "301"
	FormatHHMM               DateFormat = "401"
	FormatHHMMSS             DateFormat = "402"
	FormatHHMMSSZZZ          DateFormat = "404"
	FormatCCYY               DateFormat = "602"
	FormatCCYYMM             DateFormat = "610"
	FormatCCYYMMDDPeriod     DateFormat = "718"
	FormatCCYYMMDDHHMMPeriod DateFormat = "719"
)

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. It is empty when no segment is named.
	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
	// Err is the error behind a value [Fields] could not convert, and nil otherwise.
	Err error
}

Diagnostic locates a problem within an interchange: a structural violation found by Read or validation, or a value Fields could not convert.

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 Unmarshal added in v0.3.0

func Unmarshal(m *Message, v Unmarshaler) ([]Diagnostic, error)

Unmarshal reads m into v with a Fields located in m, and returns the diagnostics v recorded on it. They are returned even when v returns an error. A nil m gives ErrNoMessage.

Example

Unmarshal reads a message into a type and returns the diagnostics the type recorded.

package main

import (
	"fmt"
	"strings"
	"time"

	"github.com/MaldivesPorts/edifact"
)

// inHeader is the document number and date of a message, from BGM and DTM+137.
type inHeader struct {
	Number string
	Date   time.Time
}

// UnmarshalEDIFACT reads h, recording a date that will not convert on f.
func (h *inHeader) UnmarshalEDIFACT(m *edifact.Message, f *edifact.Fields) error {
	h.Number = m.Find("BGM", "").Element(2)
	h.Date = f.Time(m.Find("DTM", "137"), 1, 2, "document date")
	return nil
}

// Unmarshal reads a message into a type and returns the diagnostics the type
// recorded.
func main() {
	const plan = `UNB+UNOA:2+SENDER+RECIPIENT+260101:1200+REF1'
UNH+M1+BAPLIE:D:95B:UN'
BGM++BAY1+9'
DTM+137:2026010112AB:203'
UNT+4+M1'
UNZ+1+REF1'`

	ix, err := edifact.Read(strings.NewReader(plan))
	if err != nil {
		fmt.Println(err)
		return
	}
	var h inHeader
	diags, err := edifact.Unmarshal(ix.Message(1), &h)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(h.Number, h.Date.IsZero())
	for _, d := range diags {
		fmt.Println(d)
	}
}
Output:
BAY1 true
warning: message 1 (M1), segment 3, DTM+137: document date: element 1 component 2: format 203: value "2026010112AB" is not all digits

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 Fields added in v0.3.0

type Fields struct {
	// Diagnostics holds the warnings in the order they were recorded.
	Diagnostics []Diagnostic
	// contains filtered or unexported fields
}

Fields reads typed values out of a message's segments and records a warning for each value that is present but will not convert, so a caller can read many values and handle the problems once. An absent segment or an empty position gives the zero value and records nothing.

The zero value records warnings that name only the segment's tag and qualifier.

Example

Fields reads many values and collects a warning for each one that will not convert, located by message, segment and segment group.

package main

import (
	"errors"
	"fmt"
	"strings"

	"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() {
	const plan = `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'
EQD+CN+XXXU1234567+22G1'
LOC+147+0010206::5'
MEA+WT++KGM:8.500,5'
EQD+CN+XXXU7654321+45G1'
UNT+10+M1'
UNZ+1+REF1'`

	ix, err := edifact.ReadWithSpec(strings.NewReader(plan), bayplanSpec())
	if err != nil {
		fmt.Println(err)
		return
	}
	m := ix.Message(1)

	f := edifact.NewFields(m)
	date := f.Time(m.Find("DTM", "137"), 1, 2, "document date")
	fmt.Println(date.Format("2006-01-02 15:04"))
	for _, cell := range m.FindAllGroups("SG2") {
		where := cell.Find("LOC", "147").Component(2, 1)
		fmt.Println(where, f.Float(cell.Find("MEA", "WT"), 3, 2, "weight"))
	}
	for _, d := range f.Diagnostics {
		fmt.Println(d)
		fmt.Println("not a number:", errors.Is(d.Err, edifact.ErrNumeric))
	}
}
Output:
2026-01-01 12:00
0010204 12000
0010206 0
warning: message 1 (M1), segment 8, /SG2/MEA+WT: weight: element 3 component 2: not a valid EDIFACT numeric value: "8.500,5"
not a number: true

func NewFields added in v0.3.0

func NewFields(m *Message) *Fields

NewFields returns a Fields whose warnings are located in m: the message's position and reference, the segment's position, and the segment groups enclosing it.

func (*Fields) Addf added in v0.3.0

func (f *Fields) Addf(s *Segment, format string, args ...any)

Addf records a warning at segment s, with a message formatted from format and args, for a problem only the caller can see, such as a value that another value makes necessary. A nil s records it against the message as a whole.

Example

Addf records a problem the caller finds, such as a value that is missing where another makes it necessary.

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() {
	ix, err := edifact.ReadWithSpec(strings.NewReader(bayplan), bayplanSpec())
	if err != nil {
		fmt.Println(err)
		return
	}
	m := ix.Message(1)

	f := edifact.NewFields(m)
	for _, cell := range m.FindAllGroups("SG2") {
		loc := cell.Find("LOC", "147")
		if cell.Find("LOC", "83") == nil {
			f.Addf(loc, "cell %s: no place of delivery", loc.Component(2, 1))
		}
	}
	for _, d := range f.Diagnostics {
		fmt.Println(d)
	}
}
Output:
warning: message 1 (M1), segment 4, /SG2/LOC+147: cell 0010204: no place of delivery
warning: message 1 (M1), segment 9, /SG2/LOC+147: cell 0010206: no place of delivery

func (*Fields) Float added in v0.3.0

func (f *Fields) Float(s *Segment, elem, comp int, what string) float64

Float reads component comp of element elem, both counting from 1, as a number, the way Segment.Float does. A value that will not convert gives 0 and a warning starting with what.

func (*Fields) Int added in v0.3.0

func (f *Fields) Int(s *Segment, elem, comp int, what string) int

Int reads component comp of element elem, both counting from 1, as a whole number, the way Segment.Int does. A value that will not convert gives 0 and a warning starting with what.

func (*Fields) Time added in v0.3.0

func (f *Fields) Time(s *Segment, elem, comp int, what string) time.Time

Time reads component comp of element elem, both counting from 1, as a date and time in the format named by the component after it, the way Segment.Time does. A value that will not convert gives the zero time and a warning starting with what.

func (*Fields) TimeIn added in v0.3.0

func (f *Fields) TimeIn(s *Segment, elem, comp int, format DateFormat, what string) time.Time

TimeIn reads component comp of element elem, both counting from 1, as a date and time in the given format, the way Segment.TimeIn does. A value that will not convert gives the zero time and a warning starting with what.

func (*Fields) TimeRange added in v0.3.0

func (f *Fields) TimeRange(s *Segment, elem, comp int, what string) (start, end time.Time)

TimeRange reads component comp of element elem, both counting from 1, as a period in the format named by the component after it, the way Segment.TimeRange does. A value that will not convert gives zero times and a warning starting with what.

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) DateTime added in v0.2.0

func (g *FunctionalGroup) DateTime() (time.Time, error)

DateTime returns the date and time of preparation, read as Interchange.DateTime reads them.

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) SetDate added in v0.2.0

func (g *FunctionalGroup) SetDate(date string)

SetDate writes the date of preparation, as YYMMDD or CCYYMMDD.

func (*FunctionalGroup) SetDateTime added in v0.2.0

func (g *FunctionalGroup) SetDateTime(t time.Time)

SetDateTime writes t's date and time of preparation, in the formats the interchange's syntax version requires, as Interchange.SetDateTime does.

func (*FunctionalGroup) SetMessageType

func (g *FunctionalGroup) SetMessageType(t string)

SetMessageType writes the message type the functional group names.

func (*FunctionalGroup) SetRecipient

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

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

func (*FunctionalGroup) SetRelease added in v0.2.0

func (g *FunctionalGroup) SetRelease(release string)

SetRelease writes the message release number.

func (*FunctionalGroup) SetSender

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

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

func (*FunctionalGroup) SetTime added in v0.2.0

func (g *FunctionalGroup) SetTime(time string)

SetTime writes the time of preparation, as HHMM.

func (*FunctionalGroup) SetVersion added in v0.2.0

func (g *FunctionalGroup) SetVersion(version string)

SetVersion writes the message version number.

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.SetDate("260101")
	ix.SetTime("1200")
	ix.SetControlRef("REF1")

	g := ix.AddFunctionalGroup("BAPLIE")
	g.SetSender("SENDER", "")
	g.SetRecipient("RECIPIENT", "")
	g.SetDate("260101")
	g.SetTime("1200")
	g.SetControlRef("G1")
	g.SetAgency("UN")
	g.SetVersion("D")
	g.SetRelease("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) Charset added in v0.2.0

func (ix *Interchange) Charset() (Charset, bool)

Charset returns the syntax level UNB names. It reports false when UNB names none, or one this package does not support, such as UNOD. The identifier as written stays readable through Header().Component(1, 1).

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) DateTime added in v0.2.0

func (ix *Interchange) DateTime() (time.Time, error)

DateTime returns the date and time of preparation. UNB carries no time zone, so the result is in UnknownTimezone. A six-digit date is read as YYMMDD and an eight-digit one as 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) SetCharset added in v0.2.0

func (ix *Interchange) SetCharset(c Charset)

SetCharset writes the syntax identifier. Write refuses the zero Charset as a missing mandatory element.

Example

SetCharset declares the syntax level. Write refuses a value outside it, here lower case in UNOA, and accepts it once the interchange declares UNOB.

package main

import (
	"errors"
	"fmt"
	"io"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	ix := edifact.NewInterchange()
	ix.SetSender("SENDER", "")
	ix.SetRecipient("RECIPIENT", "")
	ix.SetDate("260101")
	ix.SetTime("1200")
	ix.SetControlRef("REF1")
	m := ix.AddMessage(edifact.Key{Type: edifact.Is("TSTMSG"), Version: edifact.Is("D"), Release: edifact.Is("95B"), Agency: edifact.Is("UN")})
	m.SetRef("M1")
	m.Add("FTX", "AAA", "", "", "Handle with care")

	err := edifact.Write(io.Discard, ix)
	fmt.Println(errors.Is(err, edifact.ErrCharset), err)

	ix.SetCharset(edifact.UNOB)
	fmt.Println(edifact.Write(io.Discard, ix))
	c, _ := ix.Charset()
	fmt.Println(c)
}
Output:
true FTX element 4 component 1: edifact: character outside the charset: 'a' (U+0061) is not in UNOA
<nil>
UNOB

func (*Interchange) SetControlRef

func (ix *Interchange) SetControlRef(ref string)

SetControlRef writes the interchange control reference.

func (*Interchange) SetDate added in v0.2.0

func (ix *Interchange) SetDate(date string)

SetDate writes the date of preparation, as YYMMDD or CCYYMMDD.

func (*Interchange) SetDateTime added in v0.2.0

func (ix *Interchange) SetDateTime(t time.Time)

SetDateTime writes t's date and time of preparation: the date as CCYYMMDD in syntax version 4 and YYMMDD below it, and the time as HHMM. t's clock is written as it is. Set the syntax version first, since Write refuses a date whose length does not match it.

Example

SetDateTime writes the date of preparation in the format the syntax version requires, and DateTime reads it back without a time zone.

package main

import (
	"fmt"
	"time"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	when := time.Date(2026, 1, 2, 15, 4, 0, 0, time.UTC)

	ix := edifact.NewInterchange()
	ix.SetDateTime(when)
	fmt.Println(ix.Date(), ix.Time())

	if err := ix.SetSyntaxVersion(4); err != nil {
		fmt.Println(err)
		return
	}
	ix.SetDateTime(when)
	fmt.Println(ix.Date(), ix.Time())

	t, err := ix.DateTime()
	fmt.Println(t.Format("2006-01-02 15:04"), t.Location(), err)
}
Output:
260102 1504
20260102 1504
2026-01-02 15:04 UNKNOWN <nil>

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) SetSyntaxVersion added in v0.2.0

func (ix *Interchange) SetSyntaxVersion(v int) error

SetSyntaxVersion writes the ISO 9735 edition. It returns ErrSyntaxVersion, and writes nothing, for a version other than 1 to 4.

func (*Interchange) SetTest

func (ix *Interchange) SetTest(test bool)

SetTest marks the interchange as test data.

func (*Interchange) SetTime added in v0.2.0

func (ix *Interchange) SetTime(time string)

SetTime writes the time of preparation, as HHMM.

func (*Interchange) SyntaxVersion

func (ix *Interchange) SyntaxVersion() int

SyntaxVersion returns the ISO 9735 edition UNB names, 1 to 4, or 0 when it names none or another value.

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 Marshaler added in v0.3.0

type Marshaler interface {
	// MarshalEDIFACT adds the receiver's segments to m, choosing every value,
	// code and format written. It returns an error for a value it cannot
	// write.
	MarshalEDIFACT(m *Message) error
}

Marshaler is a type that writes itself into a message.

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.

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 DateFormat) 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. It writes t's own clock, and its offset when the format carries one. A format whose zone is written as letters gets t in UTC.

Example

SetTime writes a time and its format qualifier into a DTM's composite, the layout Time reads back.

package main

import (
	"fmt"
	"time"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	ix := edifact.NewInterchange()
	m := ix.AddMessage(edifact.Key{})
	dtm := m.Add("DTM", "137")

	when := time.Date(2026, 1, 2, 15, 4, 0, 0, time.UTC)
	if err := dtm.SetTime(1, 2, when, edifact.FormatCCYYMMDDHHMM); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(dtm.Components(1))
}
Output:
[137 202601021504 203]

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 DateFormat) (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.

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 Unmarshaler added in v0.3.0

type Unmarshaler interface {
	// UnmarshalEDIFACT reads m into the receiver. It records values that will
	// not convert, and problems it finds, on f. It returns an error only when
	// it cannot read m at all, such as a message of a type it does not handle.
	UnmarshalEDIFACT(m *Message, f *Fields) error
}

Unmarshaler is a type that reads itself from a message.

type WriteOption

type WriteOption func(*writeConfig)

WriteOption adjusts how an interchange is serialised.

func AllowIncompleteHeaders

func AllowIncompleteHeaders() WriteOption

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

func FitCharset added in v0.2.0

func FitCharset() WriteOption

FitCharset makes Write convert characters outside the interchange's charset where it can, rather than refusing them. An accented Latin-1 letter becomes its base letter, such as é to e, Æ to AE and ß to ss, and in UNOA lower case becomes upper case. Characters it cannot convert, such as € in UNOA, are still refused with ErrCharset. By default, Write converts nothing.

Example

FitCharset converts what it can into the charset. In UNOA that means accented letters lose their accents and lower case becomes upper case.

package main

import (
	"fmt"
	"strings"

	"github.com/MaldivesPorts/edifact"
)

func main() {
	ix := edifact.NewInterchange()
	ix.SetSender("SENDER", "")
	ix.SetRecipient("RECIPIENT", "")
	ix.SetDate("260101")
	ix.SetTime("1200")
	ix.SetControlRef("REF1")
	m := ix.AddMessage(edifact.Key{Type: edifact.Is("TSTMSG"), Version: edifact.Is("D"), Release: edifact.Is("95B"), Agency: edifact.Is("UN")})
	m.SetRef("M1")
	m.Add("FTX", "AAA", "", "", "Café, Straße")

	var sb strings.Builder
	if err := edifact.Write(&sb, ix, edifact.FitCharset(), edifact.SegmentSuffixLF()); err != nil {
		fmt.Println(err)
		return
	}
	for _, line := range strings.Split(sb.String(), "\n") {
		if strings.HasPrefix(line, "FTX") {
			fmt.Println(line)
		}
	}
}
Output:
FTX+AAA+++CAFE, STRASSE'

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