Packages
hl7v2
2.9.1
3.10.1
3.9.0
3.8.0
3.7.0
3.6.0
3.5.0
3.4.0
3.3.6
3.3.5
3.3.4
3.3.3
3.3.2
3.3.1
3.3.0
3.2.0
3.1.1
3.1.0
3.0.2
3.0.1
3.0.0
2.11.0
2.10.0
2.9.1
2.9.0
2.8.2
2.8.1
2.8.0
2.7.1
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.3
2.1.2
2.1.1
2.1.0
1.4.6
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.0
1.2.0
1.1.0
1.0.0
0.6.0
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.1.0
Pure Elixir HL7 v2.x toolkit — schema-driven parsing, typed segments, message builder, MLLP transport
Current section
Files
Jump to
Current section
Files
lib/hl7v2/type.ex
defmodule HL7v2.Type do
@moduledoc """
Base behaviour for HL7v2 data types.
Every data type module (primitive and composite) implements this behaviour,
providing `parse/1` and `encode/1` for converting between wire-format
representations and typed Elixir values.
Primitive types (ST, NM, DT, etc.) work with binaries.
Composite types (CX, XPN, HD, etc.) work with lists of component strings
and return/accept structs.
## Sub-Component Separator
By default, sub-component fields are split/joined with `&`. When a message
declares a non-default sub-component separator (e.g., `$` in MSH-2 `^~\\$`),
call `with_sub_component_separator/2` to set it for the duration of a
parse or encode operation. Composite types read the active separator via
`sub_component_separator/0`.
"""
alias __MODULE__.{TS, DTM}
@compile {:inline, get_component: 2, empty_value?: 1}
@doc "Parses a wire-format value into a typed Elixir value."
@callback parse(list() | binary()) :: struct() | binary() | nil
@doc "Encodes a typed Elixir value back to wire format."
@callback encode(struct() | binary() | nil) :: list() | binary()
@sub_component_key :hl7v2_sub_component_sep
@doc """
Returns the active sub-component separator string.
Defaults to `"&"` when no separator context has been set via
`with_sub_component_separator/2`.
"""
@spec sub_component_separator() :: binary()
def sub_component_separator do
Process.get(@sub_component_key, "&")
end
@doc """
Executes `fun` with the given sub-component separator active.
The separator is stored in the process dictionary for the duration of `fun`
and restored to its previous value afterwards. This is used by segment
parse/encode to propagate the message's actual sub-component delimiter to
all composite type helpers.
"""
@spec with_sub_component_separator(binary(), (-> result)) :: result when result: term()
def with_sub_component_separator(sep, fun) when is_binary(sep) and is_function(fun, 0) do
previous = Process.get(@sub_component_key)
Process.put(@sub_component_key, sep)
try do
fun.()
after
case previous do
nil -> Process.delete(@sub_component_key)
val -> Process.put(@sub_component_key, val)
end
end
end
@doc """
Extracts a string value from a component, returning `nil` for empty/nil inputs.
Used internally by composite type parsers to normalize component access.
"""
@spec get_component(list(), non_neg_integer()) :: binary() | nil
def get_component(components, index) when is_list(components) do
case Enum.at(components, index) do
nil -> nil
"" -> nil
value when is_binary(value) -> value
subs when is_list(subs) -> rejoin_sub_components(subs)
_other -> nil
end
end
# When the raw parser has already split sub-components into a list,
# rejoin them with the sub-component delimiter so that composite type
# parsers (CX, XPN, etc.) can re-split and parse them as expected.
defp rejoin_sub_components(subs) do
if Enum.all?(subs, &(&1 == "" or is_nil(&1))) do
nil
else
Enum.join(subs, sub_component_separator())
end
end
@doc """
Pads a list of components to the given length with nils.
"""
@spec pad_components(list(), non_neg_integer()) :: list()
def pad_components(components, length) when is_list(components) do
current = length(components)
if current >= length do
components
else
components ++ List.duplicate(nil, length - current)
end
end
@doc """
Trims trailing nil/empty values from a list of components for compact encoding.
"""
@spec trim_trailing(list()) :: list()
def trim_trailing(components) when is_list(components) do
components
|> Enum.reverse()
|> Enum.drop_while(&empty_value?/1)
|> Enum.reverse()
end
defp empty_value?(nil), do: true
defp empty_value?(""), do: true
defp empty_value?([]), do: true
defp empty_value?(_), do: false
# --- Sub-component helpers ---
# Shared by all composite type modules that embed other composite types
# as sub-components (CX, XCN, XPN, XAD, PL, NDL, XON, EIP, etc.).
@doc """
Returns `true` if every field in the given struct is `nil`.
Used after parsing a sub-component to decide whether the result is
effectively empty and should be returned as `nil`.
## Examples
iex> HL7v2.Type.all_nil?(%HL7v2.Type.HD{})
true
iex> HL7v2.Type.all_nil?(%HL7v2.Type.HD{namespace_id: "MRN"})
false
"""
@spec all_nil?(struct()) :: boolean()
def all_nil?(struct) do
struct
|> Map.from_struct()
|> Map.values()
|> Enum.all?(&is_nil/1)
end
@doc """
Parses a sub-component string into the given composite type's struct.
Splits `value` on the active sub-component separator, delegates to
`module.parse/1`, and returns `nil` if all fields in the resulting
struct are `nil`.
Returns `nil` for `nil` input.
## Examples
iex> HL7v2.Type.parse_sub(HL7v2.Type.HD, "MRN&1.2.3&ISO")
%HL7v2.Type.HD{namespace_id: "MRN", universal_id: "1.2.3", universal_id_type: "ISO"}
iex> HL7v2.Type.parse_sub(HL7v2.Type.HD, nil)
nil
"""
@spec parse_sub(module(), binary() | nil) :: struct() | nil
def parse_sub(_module, nil), do: nil
def parse_sub(module, value) when is_binary(value) do
subs = String.split(value, sub_component_separator())
parsed = module.parse(subs)
if all_nil?(parsed), do: nil, else: parsed
end
@doc """
Encodes a sub-component struct back to a sub-component-separated string.
Delegates to `module.encode/1` and joins the result with the active
sub-component separator.
Returns `""` for `nil` input.
## Examples
iex> HL7v2.Type.encode_sub(HL7v2.Type.HD, %HL7v2.Type.HD{namespace_id: "MRN", universal_id: "1.2.3", universal_id_type: "ISO"})
"MRN&1.2.3&ISO"
iex> HL7v2.Type.encode_sub(HL7v2.Type.HD, nil)
""
"""
@spec encode_sub(module(), struct() | nil) :: binary()
def encode_sub(_module, nil), do: ""
def encode_sub(module, struct) do
module.encode(struct) |> Enum.join(sub_component_separator())
end
@doc """
Parses a sub-component TS (Time Stamp) value.
Like `parse_sub/2` but uses the TS-specific nil check: a TS is
considered nil when both `time` and `degree_of_precision` are nil.
## Examples
iex> HL7v2.Type.parse_sub_ts("20260322143000")
%HL7v2.Type.TS{time: %HL7v2.Type.DTM{year: 2026, month: 3, day: 22, hour: 14, minute: 30, second: 0}}
iex> HL7v2.Type.parse_sub_ts(nil)
nil
"""
@spec parse_sub_ts(binary() | nil) :: TS.t() | nil
def parse_sub_ts(nil), do: nil
def parse_sub_ts(value) when is_binary(value) do
subs = String.split(value, sub_component_separator())
ts_val = TS.parse(subs)
if all_nil?(ts_val), do: nil, else: ts_val
end
@doc """
Encodes a sub-component TS or DTM value to a sub-component-separated string.
Handles both `%TS{}` structs (encoded as sub-component list) and bare
`%DTM{}` structs (encoded directly as a date-time string).
Returns `""` for `nil` input.
## Examples
iex> HL7v2.Type.encode_sub_ts(%HL7v2.Type.TS{time: %HL7v2.Type.DTM{year: 2026, month: 3, day: 22}})
"20260322"
iex> HL7v2.Type.encode_sub_ts(%HL7v2.Type.DTM{year: 2026, month: 3, day: 22})
"20260322"
iex> HL7v2.Type.encode_sub_ts(nil)
""
"""
@spec encode_sub_ts(TS.t() | DTM.t() | nil) :: binary()
def encode_sub_ts(nil), do: ""
def encode_sub_ts(%TS{} = ts) do
case TS.encode(ts) do
[] -> ""
parts -> Enum.join(parts, sub_component_separator())
end
end
def encode_sub_ts(%DTM{} = dtm) do
DTM.encode(dtm)
end
end