Using our HL7 Tools with EDIFACT Messages

This post was originally written for Iguana 5 and may contain out of date references.

EDIFACT is Almost HL7

EDIFACT and HL7 are very similar. The main difference is how delimiters are escaped when they occur in a message. For instance, in the following EDIFACT snippet, the question-mark (?) is used to escape delimiters, or “release” them in EDIFACT parlance.

UNA:+.? `UIB+UNOA:0++Got Iguana??:163++

As you can see the third field contains an escaped question-mark, since we want it in the text.

In HL7, escape sequences are a bit longer and use letters (and sometimes numbers) in place of the delimiter or non-printable character. In the following HL7 snippet, backslashes (\) surround escape sequences, and two escaped sub-field delimiters (^ in this case) appear in the second segment.

MSH|^~\&|EPIC|EPICADT|SMS|SMSADT|199912271408|CHARRIS|ADT^A04|1817457|D|2.3|

EVN|A04|199912271408|||Do we ever! (\S\_\S\)

Otherwise the two formats are very similar: each have segments; each segment has fields, sub-fields, etc. This similarity is very important. Iguana provides many tools to manipulate, generate and extract data from HL7 messages. If we can convert EDIFACT to and from HL7, we can have those same advantages dealing with EDIFACT.

Converting EDIFACT to/from HL7

The module supplied below has three functions. The first two convert EDIFACT to/from HL7. The third normalizes the delimiters used in an EDIFACT message (i.e., you use it to change the delimiters in an EDIFACT message).

edifact.toHl7(Data)

With an EDIFACT message as input, this function will produce HL7 as output. The HL7 message created will contain an MSH segment instead of a UNA segment, but it will be mostly empty.

* There is no standard for repeating fields in EDIFACT, but some systems use them. Normally a space follows the release character in the UNA header of an EDIFACT message. If this is not a space, edifact.toHL7() will interpret it as a field-repeat delimiter when converting to HL7.

edifact.fromHl7(Data [, Header])

Once you have finished manipulating an HL7 message created by edifact.toHl7(), you can convert it back to EDIFACT with this function. You can also generate similar HL7 messages and use this function to convert them to proper EDIFACT.

Header defaults to a non-standard UNA header containing an asterisk for repeats (as described above): UNA:+.?*’

edifact.clean(Data [, Header])

If you have an EDIFACT message with delimiters you would like to change, you can supply it here with the exact Header you would like output. Header defaults to the same non-standard UNA header with an asterisk for repeats (as described above).

Changes

This module differs from a previous release in the following ways:

  1. edifact.toHl7() now emits HL7 with normal HL7 delimiters and an MSH header. This means you do not have customize the delimiters in your VMD to process its output.
  2. edifact.fromHl7() no longer requires that the delimiters in the Header provided match those in the HL7 message–that requirement was an artifact of edifact.toHl7() not emitting an MSH header.
  3. The default EDIFACT header used in edifact.fromHl7() and edifact.clean() is now non-standard. This was done to support the common extension to the standard, to support repeating fields.

The Module

------------------------------------------------------------------
--                                                              --
--  Utilities for EDIFACT Messages                              --
--                                                              --
--  Copyright (c) 2012 iNTERFACEWARE Inc.  All Rights Reserved. --
--                                                              --
------------------------------------------------------------------

local edifact = {}

-- Define the header patterns in one place.
local DEL = '([^%w ])'  -- A delimiter.
local UNA = '^UNA'..DEL:rep(3)..'(%W)(%W)'..DEL
local MSH = '^MSH'..DEL:rep(5)..'%1'  -- HL7

-- Some functions take a EDIFACT header as an
-- argument; this is used when one is not given.
local DefaultHeader = "UNA:+.?*'"

-- Convert EDIFACT to HL7 with standard EDIFACT
-- delimiters, HL7 escapes but no MSH header.
--
function edifact.toHl7(Data)
   -- These are the delimiters we want.  These will
   -- replace the input delimiters in the same order.
   local Output = {  '^',     '|',   '.',  '',     '~',     '\r'   }
   local Escape = {'\\S\\', '\\F\\', '.', '\\E\\', '\\R\\', '\\X0D\\'}
   local Header = 'MSH|^~\\&|||||||EDIFACT\r'

   -- These are the delimiters in the message.
   local Input = { select(3, Data:find(UNA)) }
   if #Input == 0 then  -- Panic!
      error('No UNA segment found.', 2)
   end

   -- We start with a simple map from the input
   -- delimiters to what we want for output.
   local BasicMap = {}
   for I,Get in ipairs(Input) do
      local Want = Output[I]
      Escape[Want] = Escape[I]
      BasicMap[Get] = Want
   end

   -- We expand this map to include checks for
   -- output delimiters in the input, and create
   -- a pattern to match all special characters.
   local Map, Pattern = { ['&']='\\T\\' }, '[%&'
   for Get,Want in pairs(BasicMap) do
      Map[Get] = Want
      Pattern = Pattern..'%'..Get
      -- Any output delimiters found in the
      -- input must be escaped in the output.
      if not BasicMap[Want] then
         Map[Want] = Escape[Want]
         Pattern = Pattern..'%'..Want
      end
   end

   -- If the input contains a release character,
   -- we expand the map and pattern further to
   -- handle escaped delimiters in the input.
   local Release = Input[4]
   if Release ~= ' ' then
      -- If a release character occurs before
      -- a non-delimiter, we leave it as-is.
      Map[Release] = Escape[Release] or Release
      -- Otherwise, we replace the pair with the
      -- second delimiter, escaped if necessary. 
      Pattern = '%'..Release..'?'..Pattern
      BasicMap, Map = Map, {}
      for Get,Want in pairs(BasicMap) do
         Map[Get] = Want
         Map[Release..Get] = Escape[Get] or Get
      end
   end

   Map[' '] = nil
   Pattern = Pattern:gsub('%% ','') .. ']'

   local Out = Data:sub(10):gsub(Pattern, Map)
   return Header..Out
end

-- Convert HL7 formatted EDIFACT to real EDIFACT
-- with the removal of HL7 escapes and replace
-- the MSH header with an EDIFACT UNA header.
--
function edifact.fromHl7(Data, NewHeader)
   local Dec, Seg = '.', '\r'
   local Ok,_,Fld,Com,Rep,Esc,Sub = Data:find(MSH)
   assert(Ok, 'Invalid/missing MSH segment.')
   Data = Data:gsub('MSH.-\r','')
   local Header = table.concat{
      'UNA',Com,Fld,Dec,Esc,Rep,Seg}
   local Escape = {
      ['\\'] = '\\',  -- Special in HL7 only.
      [Com] = Esc..Com, [Fld] = Esc..Fld,
      [Esc] = Esc..Esc, [Seg] = Esc..Seg,
      [Rep] = (Rep ~= ' ') and Esc..Rep
   }
   -- First we escape every occurance of the
   -- release character in the input.
   local Out = Data
   if #Esc > 0 then  -- Assuming we need to.
      Data:gsub('%'..Esc, Escape[Esc])
   end
   -- Then we replace all HL7 escapes with
   -- EDIFACT escapes or regular characters.
   Out = Out:gsub('(\\%u%x?%x?\\)',
      setmetatable({
            ['\\S\\'] = Escape[Com],
            ['\\F\\'] = Escape[Fld],
            ['\\E\\'] = Escape['\\'],
            ['\\R\\'] = Escape[Rep],
            ['\\T\\'] = Sub,  -- Not special.
            -- ['\\X27\\'] = Escape[Seg],
         }, {
            -- This handles other \X..\ escapes
            -- which could be present in HL7.
            __index = function(self, s)
               local Out = s:gsub('\\X(%x%x)\\',
                  function(hex)
                     local i = tonumber(hex,16)
                     return string.char(i)
                  end)
               Out = Escape[Out] or Out
               self[s] = Out  -- Memoize.
               return Out
            end
         }))
   Out = edifact.clean(Header..Out, NewHeader)
   return Out
end

-- Replace the delimiters of an EDIFACT message
-- with those in the header provided or use the
-- default EDIFACT delimiters.
--
function edifact.clean(Data, Header)
   Header = Header or DefaultHeader
   local Input  = { select(3, Data  :find(UNA)) }
   local Output = { select(3, Header:find(UNA..'$')) }
   local InpRelease, OutRelease = Input[4], Output[4]
   if not InpRelease then error('Bad UNA header in message.', 2) end
   if not OutRelease then error('Invalid output header.',     2) end
   local Escape, Map, Pattern = {}, {}, '['
   if OutRelease ~= ' ' then
      for _,Want in ipairs(Output) do
         Escape[Want] = OutRelease..Want
         Map[Want] = OutRelease..Want
         Pattern = Pattern..'%'..Want
      end
   end
   for I,Get in ipairs(Input) do
      local Want = Output[I]
      Map[Get] = Want
      Pattern = Pattern..'%'..Get
   end
   if InpRelease ~= ' ' then
      Pattern = '%'..InpRelease..'?'..Pattern
      for I,Get in ipairs(Input) do
         Map[InpRelease..Get] = Escape[Get] or Get
      end
   end
   Map[' '] = nil
   Pattern = Pattern:gsub('%% ','') .. ']'
   local Out = Data:sub(10):gsub(Pattern,Map)
   return Header..Out
end

return edifact

Leave A Comment?