lib/hl7/segment.ex

defmodule HL7.Segment do
  require Logger

  @moduledoc """
  Filter, parse and modify individual HL7 segments and their fragments.
  """

  @type raw_hl7 :: String.t() | HL7.RawMessage.t()
  @type fragment_hl7 :: String.t() | [list() | String.t()]
  @type segment_hl7 :: [fragment_hl7()]
  @type parsed_hl7 :: [segment_hl7()] | HL7.Message.t()
  @type content_hl7 :: raw_hl7() | parsed_hl7()

  @doc ~S"""
  Updates content within a parsed HL7 segment, returning a modified segment whose data has been transformed at the given 
  indices (starting at 1 as with HL7's convention). The `transform` can be either a `string`, `list` or `fn old_data -> new_data`.
  """

  @spec replace_part(
          segment_hl7(),
          fragment_hl7() | (fragment_hl7() -> fragment_hl7()),
          pos_integer(),
          pos_integer() | nil,
          pos_integer() | nil,
          pos_integer() | nil
        ) :: segment_hl7()

  def replace_part(
        segment,
        transform,
        field,
        repetition \\ nil,
        component \\ nil,
        subcomponent \\ nil
      )

  def replace_part(segment, transform, field, repetition, component, subcomponent)
      when is_integer(field) and is_function(transform, 1) do
    indices = to_indices(field, repetition, component, subcomponent)
    replace_fragment(segment, indices, transform, true)
  end

  def replace_part(segment, transform, field, repetition, component, subcomponent)
      when is_integer(field) and (is_binary(transform) or is_list(transform)) do
    indices = to_indices(field, repetition, component, subcomponent)
    replace_fragment(segment, indices, fn _data -> transform end, true)
  end

  @doc false

  # used by HL7.Message to update segments with list-based indices

  @spec replace_fragment(list() | String.t(), list(), function(), boolean()) ::
          list() | String.t()
  def replace_fragment(data, [], transform, is_field) when is_function(transform, 1) do
    transform.(data) |> unwrap_binary_field(is_field)
  end

  def replace_fragment(data, [i | remaining_indices], transform, is_field)
      when is_binary(data) and is_integer(i) and is_function(transform, 1) do
    [data | empty_string_list(i)]
    |> List.update_at(0, fn d ->
      replace_fragment(d, remaining_indices, transform, false) |> unwrap_binary_field(is_field)
    end)
    |> Enum.reverse()
  end

  def replace_fragment(data, [i | remaining_indices], transform, is_field)
      when is_list(data) and is_integer(i) and is_function(transform, 1) do
    count = Enum.count(data)

    case i < count do
      true ->
        List.update_at(data, i, fn d ->
          replace_fragment(d, remaining_indices, transform, false)
          |> unwrap_binary_field(is_field)
        end)

      false ->
        data
        |> Enum.reverse()
        |> empty_string_list(i - count + 1)
        |> List.update_at(0, fn d ->
          replace_fragment(d, remaining_indices, transform, false)
          |> unwrap_binary_field(is_field)
        end)
        |> Enum.reverse()
    end
  end

  @doc false
  def get_part_by_indices(data, []) do
    data
  end

  def get_part_by_indices(data, [i | remaining_indices]) do
    case data do
      nil ->
        data

      _ when is_binary(data) ->
        data

      _ when is_integer(i) and is_list(data) ->
        Enum.at(data, i) |> get_part_by_indices(remaining_indices)
    end
  end

  @doc ~S"""
  Extracts content from a parsed HL7 segment,
  returning nested data by applying each supplied index in turn. Please note that HL7 indices start at 1.
  """

  @spec get_part(
          segment_hl7() | fragment_hl7(),
          field :: pos_integer(),
          repetition :: pos_integer() | nil,
          component :: pos_integer() | nil,
          subcomponent :: pos_integer() | nil
        ) :: nil | list() | binary()

  def get_part(data, field, repetition \\ nil, component \\ nil, subcomponent \\ nil)
      when is_integer(field) and
             (is_integer(repetition) or is_nil(repetition)) and
             (is_integer(component) or is_nil(component)) and
             (is_integer(subcomponent) or is_nil(subcomponent)) do
    indices = to_indices(field, repetition, component, subcomponent)
    get_part_by_indices(data, indices)
  end

  def leftmost_value([]) do
    nil
  end

  def leftmost_value([h | _]) do
    leftmost_value(h)
  end

  def leftmost_value(d) do
    d
  end

  @spec unwrap_binary_field(list() | String.t(), boolean()) :: list() | String.t()
  defp unwrap_binary_field([text], _is_field = true) when is_binary(text) do
    text
  end

  defp unwrap_binary_field(data, is_field) when is_boolean(is_field) do
    data
  end

  @spec empty_string_list(non_neg_integer()) :: [String.t()]
  defp empty_string_list(n) when is_integer(n) do
    empty_string_list([], n)
  end

  @spec empty_string_list([String.t()], non_neg_integer()) :: [String.t()]
  defp empty_string_list(list, 0) do
    list
  end

  defp empty_string_list(list, n) do
    empty_string_list(["" | list], n - 1)
  end

  defp to_indices(field, repetition, component, subcomponent) do
    [field + 1, repetition, component, subcomponent]
    |> Enum.take_while(fn i -> i != nil end)
    |> Enum.map(fn i -> i - 1 end)
  end
end