{"id":13508959,"url":"https://github.com/jcomellas/ex_hl7","last_synced_at":"2025-04-15T00:31:46.131Z","repository":{"id":62429125,"uuid":"37812140","full_name":"jcomellas/ex_hl7","owner":"jcomellas","description":"HL7 Parser for Elixir","archived":false,"fork":false,"pushed_at":"2019-12-31T22:43:24.000Z","size":187,"stargazers_count":41,"open_issues_count":1,"forks_count":13,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-28T12:51:14.982Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jcomellas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-06-21T14:36:37.000Z","updated_at":"2024-07-06T10:43:49.000Z","dependencies_parsed_at":"2022-11-01T20:07:04.469Z","dependency_job_id":null,"html_url":"https://github.com/jcomellas/ex_hl7","commit_stats":null,"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcomellas%2Fex_hl7","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcomellas%2Fex_hl7/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcomellas%2Fex_hl7/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcomellas%2Fex_hl7/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jcomellas","download_url":"https://codeload.github.com/jcomellas/ex_hl7/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248984331,"owners_count":21193727,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T02:01:01.035Z","updated_at":"2025-04-15T00:31:45.571Z","avatar_url":"https://github.com/jcomellas.png","language":"Elixir","funding_links":[],"categories":["Protocols"],"sub_categories":[],"readme":"# HL7 Parser for Elixir\n\n## Overview\n\nHealth Level 7 ([HL7](http://www.hl7.org/)) is a protocol (and an organization) designed to model\nand transfer health-related data electronically.\n\nThis parser has support for the HL7 version 2.x syntax. It was tested using v2.4-compliant data,\nbut it should also work with any v2.x messages. It doesn't support the XML mappings that were\ncreated for HL7 v3.x, though.\n\nIt also has support for custom [segment](lib/ex_hl7/segment.ex) and\n[composite](lib/ex_hl7/composite.ex) field definitions though an easy-to-use DSL built on top of\nElixir macros. A set of preset default [segments](lib/ex_hl7/segment/default) and\n[composites](lib/ex_hl7/composite/default) are available to be used with the parser.\n\nThe parser was designed to make the interaction with HL7 as smooth as possible, but its use\nrequires at least moderate knowledge of the HL7 messaging standards.\n\n## Requirements\n\nThis application waslast updated and tested using Elixir 1.9.4 (and Erlang 22.2) but there shouldn't\nbe any special dependency that prevents it from working with older or later versions.\n\nThere are no runtime dependencies on external projects. The parser will make use of the\n[Logger](http://elixir-lang.org/docs/stable/logger/) application included in Elixir to output\nwarnings when reading or writing to fields that are not present in the corresponding segment's\ndefinition.\n\n## Installation\n\nYou can use *ex_hl7* in your projects by adding it to your `mix.exs` dependencies:\n\n```elixir\ndef deps do\n  [{:ex_hl7, \"~\u003e 1.0.0\"},\nend\n```\n\n## Contributing\n\nOnly a small subset of the HL7 segments and composite fields are included in the project. You can\nalways roll your own definitions in your project, but if you feel your changes would help others,\nplease fork the repository, add whatever you need and send a pull-request.\n\n## Encoding Rules\n\nAn HL7 message in its v2.x wire-format is actually a collection of concatenated segments, each\nterminated by a carriage-return (0x0d) character. Each segment is a collection of fields separated\nby a custom separator character (`|` by default). Depending on the type of the field, each field\ncan have multiple optional repetitions (separated by `~` by default), can be made out of multiple\ncomponents (separated by `^` by default) where each of them can also have subcomponents (separated\nby `\u0026` by default).\n\nThis structure maps nicely to a k-ary tree. For example, given the following segment:\n\n    OBX|1|CE|71020\u0026IMP|1|.61^RUL^ACR~.212^Bronchopneumonia^ACR\\r\n\nWe could represent it as the following subtree within a message:\n\n```\nsegment                            OBX\n                                    |\nfields        [1]--[2]--------[3]---+------[4]------------[5]\n              /     |          |            |               \\\n             1     \"CE\"        |           \"1\"               |\n                               |                             |\ncomponents                    [1]                [1]--------[2]---------[3]\n                               |                 /           |            \\\n                               |               0.61        \"RUL\"         \"ACR\"\n                               |              0.212  \"Bronchopneumonia\"  \"ACR\"\nsubcomponents             [1]--+--[2]\n                          /         \\\n                      \"71020\"      \"IMP\"\n```\n\nThe field on sequence 5 contains two repetitions of a composite field.\n\n*Note*: the indexes used for the fields adn other items are 1-based because this value is actually\nthe sequence number assigned by HL7 to identify the field.\n\nThe input and output of the high level functions used to read or write a message (e.g. `HL7.read/2`,\n`HL7.write/2`) is affected by a boolean argument named `trim`. This value changes the input and\noutput from the lower level functions of the parser. If set to `true`, some trailing optional items\nand separators will be omitted from the decoded or encoded message.\n\nFor example, a field that was originally read as:\n\n    504599^223344\u0026\u0026IIN\u0026^~\n\nWould be written in the following way when `trim` is set to `true`:\n\n    504599^223344\u0026\u0026IIN\n\nBoth representations are correct, given that HL7 allows trailing items that are empty to be omitted.\n\n## Single Value Fields\n\nHL7 supports many types of single value (scalar, non-composite) fields. This parser maps all of them\n(including those that are identifiers in a table) to a few data types:\n\n* `nil`: the null value from HL7 (`\"\\\"\\\"\"`).\n* `:string`: text value with no conversion performed on it. If the text contains characters that\n  may overlap any message delimiter, it should be modified following the HL7 escaping rules\n  (see `HL7.escape/2` and `HL7.unescape/2`).\n* `:integer`: integer number\n* `:float`: floating-point number with a dot (`.`) as decimal point; its text representation can be\n  that of an integer (i.e. with no decimal point).\n* `:date`: date in the `YYYYMMDD` format that is represented as a\n  [Date](https://hexdocs.pm/elixir/Date.html) struct.\n* `:datetime`: date/time in the `YYYYMMDD[hhmm[ss]]` format represented as a\n  [NaiveDateTime](https://hexdocs.pm/elixir/NaiveDateTime.html) struct. If the time is not present,\n  the hour, minutes and seconds will be set to `0`.\n\n## Composite Fields\n\nHL7 supports many types of composite fields and not all of them are included in this project, so to\nsimplify their use there are some macros in the `HL7.Composite.Spec` module that help you easily\ndefine new ones.\n\nThis parser allows you to define composite fields as modules and, given the following definition\nfrom the HL7 standard:\n\n\u003e 2.9.3 CE - coded element\n\u003e\n\u003e \u003cidentifier (ST)\u003e ^ \u003ctext (ST)\u003e ^ \u003cname of coding system (IS)\u003e ^\n\u003e \u003calternate identifier (ST)\u003e ^ \u003calternate text (ST)\u003e ^\n\u003e \u003cname of alternate coding system (IS)\u003e\n\nThey can be defined in the following way:\n\n```elixir\nuse HL7.Composite.Spec\n\ndefmodule HL7.Composite.CE do\n  composite do\n    component :id,                type: :string\n    component :text,              type: :string\n    component :coding_system,     type: :string\n    component :alt_id,            type: :string\n    component :alt_text,          type: :string\n    component :alt_coding_system, type: :string\n  end\nend\n```\n\nUsing this information the parser will build an internal specification for the composite that will\nbe used to resolve the fields referencing the composite during compile-time.\n\nEach component has a name represented by an atom with the following properties:\n\n* `type`: atom corresponding to the data type of the value (see\n  [single value fields](#single-value-fields)) or to a composite field's module name\n  (e.g. `HL7.Composite.Default.CE`).\n* `default`: optional default value; if not defined it will be set to an empty string (`\"\"`).\n\nComposite fields can also be nested, and you can do it in the following way:\n\n```elixir\nuse HL7.Composite.Spec\n\nalias HL7.Composite.CE\n\ndefmodule HL7.Composite.CQ do\n  composite do\n    component :quantity,          type: :integer\n    component :units,             type: CE\n  end\nend\n```\n\n## Segments\n\nAs with composite fields, not all HL7 segments are provided with the project, so there is also a\nset of macros in the `HL7.Segment.Spec` module that help define new segments.\n\nSegments are also exposed as structs and can be defined in this way:\n\n```elixir\nuse HL7.Segment.Spec\n\ndefmodule HL7.Segment.OBX do\n  alias HL7.Composite.CE\n\n  segment \"OBX\" do\n    field :set_id,                          seq:  1, type: :integer, len: 4\n    field :value_type,                      seq:  2, type: :string, len: 10\n    field :observation_id,                  seq:  3, type: {CE, :id}, len: 14\n    field :observation_coding_system,       seq:  3, type: {CE, :coding_system}, len: 8\n    field :observation_sub_id,              seq:  4, type: :string, len: 20\n    field :observation_value_id,            seq:  5, type: {CE, :id}, len: 14\n    field :observation_value_coding_system, seq:  5, type: {CE, :coding_system}, len: 8\n    field :observation_status,              seq: 11, type: :string, len: 1\n  end\nend\n```\n\nThis segment will be exposed as the following struct:\n\n```elixir\ndefstruct :set_id, :value_type, :observation_id, :observation_coding_system, :observation_sub_id,\n          :observation_value_id, :observation_value_coding_system, :observation_status\n```\n\nEach field has a name represented by an atom and has the following properties:\n\n* `seq`: sequence (1-based index) of the field in the segment.\n* `type`: atom corresponding to the data type of the value (see\n  [single value fields](#single-value-fields)) or to a composite field's module name\n  (e.g. `HL7.Composite.Default.CE`). The composite fields will be resolved during compile-time and\n  their specification will be added to the corresponding field specification that can be accessed\n  through the segment module's `spec/0` function.\n* `len`: maximum length of the serialized field.\n* `default`: optional default value; if not defined it will be set to an empty string (`\"\"`) for\n  all types when creating a new segment.\n\n*Note*: not all of the fields need to be defined in a segment. Segments can be \"sparse\" and the\nfields can be defined in an order that is not their sequence order. This means that if a segment\ncontaining an undefined field is parsed, **that field will be lost** when writing/serializing the\nsegment back to its wire-format.\n\n## Messages\n\nA parsed HL7 message is represented as a list of segment structs, so you can use the functions\nfrom the `Enum` and `List` modules to retrieve data or modify them.\n\nThe `HL7` module has several functions that can be used with messages. The examples below assume\nthat the following HL7 message is being used:\n\n```elixir\nbuffer =\n  \"MSH|^~\\\\\u0026|BLAKEMD|EWHIN|MSC|EWHIN|19940110105307||RQA^I08|BLAKEM7898|P|2.4|||NE|AL\\r\" \u003c\u003e\n  \"PRD|RP|BLAKE^BEVERLY^^^DR^MD|N. 12828 NEWPORT HIGHWAY^^MEAD^WA^99021| ^^^BLAKEMD\u0026EWHIN^^^^^BLAKE MEDICAL CENTER|BLAKEM7899\\r\" \u003c\u003e\n  \"PRD|RT|WSIC||^^^MSC\u0026EWHIN^^^^^WASHINGTON STATE INSURANCE COMPANY\\r\" \u003c\u003e\n  \"PID|||402941703^9^M10||BROWN^CARY^JOE||19600309||||||||||||402941703\\r\" \u003c\u003e\n  \"IN1|1|PPO|WA02|WSIC (WA State Code)|11223 FOURTH STREET^^MEAD^WA^99021^USA|ANN MILLER|509)333-1234|987654321||||19901101||||BROWN^CARY^JOE|1|19600309|N. 12345 SOME STREET^^MEAD^WA^99021^USA|||||||||||||||||402941703||||||01|M\\r\" \u003c\u003e\n  \"DG1|1|I9|569.0|RECTAL POLYP|19940106103500|0\\r\" \u003c\u003e\n  \"PR1|1|C4|45378|Colonoscopy|19940110105309|00\\r\"\n```\n\nYou can read/parse a message from a binary in the following way:\n\n```elixir\n{:ok, message} = HL7.read(buffer)\n```\n\nRetrieve a specific repetition of a segment:\n\n```elixir\nalias HL7.Segment.Default.PRD\n\n%PRD{role_id: role_id} = prd = HL7.segment(message, \"PRD\", 1)\n\"PRD\" = HL7.segment_id(prd)\n\"RT\" = role_id\n```\n\nInsert segments:\n\n```elixir\nalias HL7.Segment.Default.{AUT, PR1}\nalias HL7.Composite.Default.{CE, EI}\n\npr1 = HL7.segment(message, \"PR1\")\naut = %AUT{plan_id: \"PPO\", company_id: \"WA02\",\n           effective_date: ~D[1994-01-10],\n           expiration_date: ~D[1994-05-10],\n           authorization_id: \"123456789\"}\nmessage = HL7.insert_before(message, \"PR1\", 0, [pr1, aut])\nmessage = HL7.insert_after(message, \"PR1\", 1, aut)\n```\n\nReplace segments:\n\n```elixir\nmessage = HL7.replace(message, \"PR1\", 0, %PR1{pr1 | set_id: 2})\n```\n\nDelete segments:\n\n```elixir\nmessage = HL7.delete(message, \"PR1\", 1)\nmessage = HL7.delete(message, \"AUT\", 1)\n```\n\nWrite a message into the HL7 wire format:\n\n```elixir\niobuf = HL7.write(message, output_format: :wire, trim: true)\n```\n\nWrite a message as text to standard output:\n\n```elixir\nIO.puts(HL7.write(message, output_format: :text, trim: true))\n```\n\n## Example\n\nThis is a basic example of a pre-authorization request with referral to another provider (`RQA^I08`)\nthat shows how to use the parser. For more information, please check the rest of the sections above.\n\n```elixir\ndefmodule Authorizer do\n  alias HL7.Segment.Default.{AUT, MSA, MSH, PID, PRD}\n  alias HL7.Composite.Default.{CE, CM_MSH_9, CP, EI, MO}\n\n  def authorize(req) do\n    message_type = HL7.segment(req, \"MSH\").message_type\n    authorize(req, message_type.id, message_type.trigger_event)\n  end\n\n  def authorize(req, \"RQA\", \"I08\") do\n    msh = HL7.segment(req, \"MSH\")\n    msa = %MSA{\n            ack_code: \"AA\",\n            message_control_id: msh.message_control_id\n          }\n    msh = %MSH{msh |\n            sending_app_id: msh.receiving_app_id,\n            sending_facility_id: msh.receiving_facility_id,\n            sending_facility_universal_id: msh.receiving_facility_universal_id,\n            sending_facility_universal_id_type: msh.receiving_facility_universal_id_type,\n            receiving_app_id: msh.sending_app_id,\n            receiving_facility_id: msh.sending_facility_id,\n            receiving_facility_universal_id: msh.sending_facility_universal_id,\n            receiving_facility_universal_id_type: msh.sending_facility_universal_id_type,\n            message_datetime: NaiveDateTime.utc_now(),\n            # RPA^I08\n            message_type_id: \"RPA\",\n            message_control_id: Base.encode32(:crypto.rand_bytes(10)),\n            accept_ack_type: \"ER\",\n            app_ack_type: \"ER\"\n          }\n    aut = %AUT{\n            plan_id: \"PPO\",\n            company_id: \"WA02\",\n            company_name: \"WSIC (WA State Code)\",\n            effective_date: ~D[1994-01-10],\n            expiration_date: ~D[1994-05-10],\n            authorization_id: \"123456789\",\n            reimbursement_limit: 175.0,\n            requested_treatments: 1\n          }\n    req\n    |\u003e HL7.replace(\"MSH\", msh)\n    |\u003e HL7.insert_after(\"MSH\", msa)\n    |\u003e HL7.insert_after(\"PR1\", 0, aut)\n  end\n\n  def patient(%PID{last_name: last_name, first_name: first_name}) do\n    \"Patient: #{first_name} #{last_name}\"\n  end\n  def patient(_pid) do\n    nil\n  end\n\n  def practice([dg1, pr1]) do\n    \"\"\"\n    Diagnosed with: #{dg1.description}\n    Treatment: #{pr1.description}\n    \"\"\"\n  end\n\n  def providers(prds), do:\n    providers(prds, [])\n\n  def providers([%PRD{} = prd | tail], acc) do\n    info = \"\"\"\n    #{role_label(prd.role_id)}:\n      ##{prd.first_name} #{prd.last_name}\n      #{prd.street}\n      #{prd.city}, #{prd.state} #{prd.postal_code}\n    \"\"\"\n    providers(tail, [info | acc])\n  end\n  def providers([_prd | tail], acc) do\n    providers(tail, acc)\n  end\n  def providers([], acc) do\n    Enum.reverse(acc)\n  end\n\n  def role_label(\"RP\"), do: \"By\"\n  def role_label(\"RT\"), do: \"And referred to\"\nend\n\nimport Authorizer\n\nbuf =\n  \"MSH|^~\\\\\u0026|BLAKEMD|EWHIN|MSC|EWHIN|19940110105307||RQA^I08|BLAKEM7898|P|2.4|||NE|AL\\r\" \u003c\u003e\n  \"PRD|RP|BLAKE^BEVERLY^^^DR^MD|N. 12828 NEWPORT HIGHWAY^^MEAD^WA^99021| ^^^BLAKEMD\u0026EWHIN^^^^^BLAKE MEDICAL CENTER|BLAKEM7899\\r\" \u003c\u003e\n  \"PRD|RT|WSIC||^^^MSC\u0026EWHIN^^^^^WASHINGTON STATE INSURANCE COMPANY\\r\" \u003c\u003e\n  \"PID|||402941703^9^M10||BROWN^CARY^JOE||19600309||||||||||||402941703\\r\" \u003c\u003e\n  \"IN1|1|PPO|WA02|WSIC (WA State Code)|11223 FOURTH STREET^^MEAD^WA^99021^USA|ANN MILLER|509)333-1234|987654321||||19901101||||BROWN^CARY^JOE|1|19600309|N. 12345 SOME STREET^^MEAD^WA^99021^USA|||||||||||||||||402941703||||||01|M\\r\" \u003c\u003e\n  \"DG1|1|I9|569.0|RECTAL POLYP|19940106103500|0\\r\" \u003c\u003e\n  \"PR1|1|C4|45378|Colonoscopy|19940110105309|00\\r\"\n\n{:ok, req} = HL7.read(buf, input_format: :wire)\n\n# Print authorization request data\nreq |\u003e HL7.segment(\"PID\") |\u003e patient() |\u003e IO.puts()\nreq |\u003e HL7.paired_segments([\"DG1\", \"PR1\"]) |\u003e practice() |\u003e IO.puts()\nreq |\u003e Enum.filter(\u0026(HL7.segment_id(\u00261) === \"PRD\")) |\u003e providers() |\u003e IO.puts()\n\n# Create an authorized response and print it\nreq |\u003e authorize() |\u003e HL7.write(output_format: :text, trim: true) |\u003e IO.puts()\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcomellas%2Fex_hl7","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjcomellas%2Fex_hl7","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcomellas%2Fex_hl7/lists"}