Current section

Files

Jump to
hl7v2 lib hl7v2 separator.ex
Raw

lib/hl7v2/separator.ex

defmodule HL7v2.Separator do
@moduledoc """
Detects and manages HL7v2 message delimiters from MSH-1/MSH-2.
Every HL7v2 message declares its delimiter set in the first 9 characters of the
MSH segment: `MSH` (3 chars) + field separator (MSH-1, 1 char) + encoding
characters (MSH-2, 4 or 5 chars). The defaults are `|^~\\&` but the standard
allows any single-byte characters.
HL7 v2.7+ allows an optional 5th encoding character: the **truncation character**.
When present (e.g., `^~\\&#`), it indicates that field values ending with this
character were truncated. It is a display hint, not a delimiter.
## Examples
iex> HL7v2.Separator.default()
%HL7v2.Separator{field: ?|, component: ?^, repetition: ?~, escape: ?\\\\, sub_component: ?&, truncation: nil, segment: ?\\r}
iex> {:ok, sep} = HL7v2.Separator.from_msh("MSH|^~\\\\&|SENDING_APP|")
iex> sep.field
?|
iex> {:ok, sep} = HL7v2.Separator.from_msh("MSH|^~\\\\&#|SENDING_APP|")
iex> sep.truncation
?#
"""
defstruct field: ?|,
component: ?^,
repetition: ?~,
escape: ?\\,
sub_component: ?&,
truncation: nil,
segment: ?\r
@type t :: %__MODULE__{
field: non_neg_integer(),
component: non_neg_integer(),
repetition: non_neg_integer(),
escape: non_neg_integer(),
sub_component: non_neg_integer(),
truncation: non_neg_integer() | nil,
segment: non_neg_integer()
}
@doc """
Returns the default HL7v2 delimiter set (`|^~\\&` with CR segment terminator, no truncation character).
"""
@spec default() :: t()
def default, do: %__MODULE__{}
@doc """
Extracts delimiters from an MSH header binary.
MSH-1 is the single character immediately after `"MSH"` (the field separator).
MSH-2 is the next 4 characters (component, repetition, escape, sub-component),
written as a literal string and NOT delimited. In HL7 v2.7+, a 5th character
(truncation) may follow. The truncation character is distinguished from the
start of the next field by checking whether the 5th byte equals the field
separator: if it does not, it is the truncation character.
Returns `{:ok, separator}` or `{:error, reason}`.
## Examples
iex> {:ok, sep} = HL7v2.Separator.from_msh("MSH|^~\\\\&|SendApp|")
iex> sep.field
?|
iex> sep.component
?^
iex> {:ok, sep} = HL7v2.Separator.from_msh("MSH|^~\\\\&#|SendApp|")
iex> sep.truncation
?#
"""
@spec from_msh(binary()) :: {:ok, t()} | {:error, term()}
# Extended: 5 encoding chars with truncation character (v2.7+).
# The 5th byte after MSH-1 is a truncation char if it is NOT the field separator.
# The byte after the truncation char must be the field separator or end of input.
def from_msh(<<"MSH", field, c, r, e, s, t, rest::binary>>) when t != field do
case rest do
<<next, _::binary>> when next != field ->
# 6+ encoding characters — overlong, reject
{:error, :invalid_encoding_characters}
_ ->
build_separator(field, c, r, e, s, t)
end
end
# Standard: 4 encoding chars (MSH-2). The next byte must be the field separator
# or the message ends right after the encoding characters.
def from_msh(<<"MSH", field, c, r, e, s, rest::binary>>) do
case rest do
"" ->
build_separator(field, c, r, e, s, nil)
<<^field, _::binary>> ->
build_separator(field, c, r, e, s, nil)
_ ->
# Next byte is not the field separator and was not caught by the
# truncation clause above — means it equals the field separator in
# a duplicate-delimiter scenario, or something else is wrong.
# This case is unreachable in practice because the truncation clause
# above handles t != field, but we keep it for safety.
{:error, :invalid_encoding_characters}
end
end
def from_msh(<<"MSH", _::binary>>) do
{:error, :insufficient_encoding_characters}
end
def from_msh(_) do
{:error, :not_msh}
end
defp build_separator(field, c, r, e, s, t) do
encoding = [c, r, e, s | if(t, do: [t], else: [])]
cond do
field in encoding ->
# An encoding character equals the field separator — this means
# the MSH-2 declaration is effectively too short or malformed.
{:error, :invalid_encoding_characters}
length(encoding) != length(Enum.uniq(encoding)) ->
{:error, :duplicate_delimiters}
true ->
{:ok,
%__MODULE__{
field: field,
component: c,
repetition: r,
escape: e,
sub_component: s,
truncation: t,
segment: ?\r
}}
end
end
@doc """
Returns the encoding characters string (MSH-2 value) for this separator set.
When a truncation character is present (v2.7+), the returned string is 5 characters.
## Examples
iex> HL7v2.Separator.encoding_characters(HL7v2.Separator.default())
"^~\\\\&"
"""
@spec encoding_characters(t()) :: binary()
def encoding_characters(%__MODULE__{truncation: nil} = sep) do
<<sep.component, sep.repetition, sep.escape, sep.sub_component>>
end
def encoding_characters(%__MODULE__{truncation: t} = sep) when not is_nil(t) do
<<sep.component, sep.repetition, sep.escape, sep.sub_component, t>>
end
end