Handling broken MSH segments

This article was originally written for Iguana 5 so it uses version 5 screenshots and may have some out of date references. For example the code in the screenshots uses the the stringutil module to add the string.split function – this is no longer needed as it is now a builtin function in Iguana 6.

Please be aware that the code in this article is intended to give you ideas, it is not production ready. If you need help with similar problems please contact us at support@interfaceware.com.

My question now is whether we could automatically detect the off by one problem? Perhaps a little routine which could parse the MSH segment looking for where the event codes are. Could we get the problem solved in an almost plug and play manner?

This problem that was brought to our attention by one of our customers. He’s encountered a number of off by one problems. One was a counter party that just didn’t know how to count fields. He guessed they knew MSH-9 was message type, so counted backwards but didn’t know that MSH-1=”|”. That led them to MSH-1=”MSH” and PV1-1=”PV1″, so patient class would be after the first separator. The end result being that the MSH segment was correct, and all the other segments off by one. The mis-understanding by the counter party sounds like something that could easily happen in practice. It’s a subtle problem which was compounded in this case with the implementation putting data into random fields (a mapping issue that we do not address here). Off by one issues are complex and the code below does not automatically solve them all, instead it demonstrates an approach to fixing the issues. This is an HL7 message with a valid MSH segment:

MSH|^~&||Lab|Main HIS|St. Micheals|20110213144932||ADT^A03|9B38584D9903051F0D2B52CC0148965775D2D23FE4C51BE060B33B6ED27DA820|P|2.6
EVN||20110213144532||||20110213145902|
PID||4525285^^^ADT1||Smith^Tracy||19980210|F||Martian|86 Yonge St.^^ST. LOUIS^MO^51460|||||||10-346-6|284-517-569|
NK1||Smith^Gary|Second Cousin|
PV1||E||||||5101^Garland^Mary^F^^DR|||||||||||1318095^^^ADT1|||||||||||||||||||||||||20110213144956|
OBX|||WT^WEIGHT||102|pounds|
OBX|||HT^HEIGHT||32|cm|

But sometimes, a vendor sends a message with an extra field at the beginning of every segment, after the name:

MSH|^~&|||Lab|Main HIS|St. Micheals|20110213144932||ADT^A03|9B38584D9903051F0D2B52CC0148965775D2D23FE4C51BE060B33B6ED27DA820|P|2.6
EVN|||20110213144532||||20110213145902|
PID|||4525285^^^ADT1||Smith^Tracy||19980210|F||Martian|86 Yonge St.^^ST. LOUIS^MO^51460|||||||10-346-6|284-517-569|
NK1|||Smith^Gary|Second Cousin|
PV1|||E||||||5101^Garland^Mary^F^^DR|||||||||||1318095^^^ADT1|||||||||||||||||||||||||20110213144956|
OBX||||WT^WEIGHT||102|pounds|
OBX||||HT^HEIGHT||32|cm|

Or one less field:

MSH|^~&|Lab|Main HIS|St. Micheals|20110213144932||ADT^A03|9B38584D9903051F0D2B52CC0148965775D2D23FE4C51BE060B33B6ED27DA820|P|2.6
EVN|20110213144532||||20110213145902|
PID|4525285^^^ADT1||Smith^Tracy||19980210|F||Martian|86 Yonge St.^^ST. LOUIS^MO^51460|||||||10-346-6|284-517-569|
NK1|Smith^Gary|Second Cousin|
PV1|E||||||5101^Garland^Mary^F^^DR|||||||||||1318095^^^ADT1|||||||||||||||||||||||||20110213144956|
OBX||WT^WEIGHT||102|pounds|
OBX||HT^HEIGHT||32|cm|

It would be nice if we could detect this, and either add or remove the field. Below is a module that will do just that.

We added some code to doctor a single message so we could show the fixes in a one picture. You may find it easier to just change the messages manually. Then you can lose the kludgy msgAddField() and msgRemoveField() and simplify the code like this:

Cut and paste the code from below. This code goes in main:

require 'oneoff'

function main(Data)

   -- A normal message will not be changed
   local msg = oneoff.adjust(Data)  
   trace(msg)
   assert(msg == Data)

   -- Add a  field - then fix it
   msg = msgAddField(Data)
   trace(msg)
   msg = oneoff.adjust(msg)
   trace(msg)
   assert(msg == Data)

   -- Remove a field - then fix it
   -- Only removes a blank 3rd field
   msg = msgRemoveField(Data)
   trace(msg)
   msg = oneoff.adjust(msg)
   trace(msg)
   assert(msg == Data)
end

function msgAddField(Msg)
   Msg = oneoff.genMsg(oneoff.forEachSegment(oneoff.parse(Msg),oneoff.addField))
   return Msg
end

function msgRemoveField(Msg)
   local T = oneoff.parse(Msg)
   if T.segments[1][3]=='' then -- only remove a blank 3rd field
      Msg = oneoff.genMsg(oneoff.forEachSegment(oneoff.parse(Msg),oneoff.removeField))
   end
   return Msg
end

Or the simple version:

require 'oneoff'

function main(Data)

   -- Fix message
   local msg = oneoff.adjust(Data)  
   trace(msg)
   assert(msg == Data)

end

This goes in the oneoff module:

oneoff = {}

-- iterates through segments, applies the 
-- operation to the segment
function oneoff.forEachSegment(Parsed, Operation)
   for i,v in pairs(Parsed.segments) do Operation(Parsed,i,v) end
   return Parsed
end

-- Takes the output from parse and reconstructs the
-- hl7 message
function oneoff.genMsg(Parsed)
   local output = ''
   trace(#Parsed.segments)
   for i,Segment in pairs(Parsed.segments) do 
      if i < #Parsed.segments then
         output = output..table.concat(Segment,Parsed.delimiter)..'r'
      else
         output = output..table.concat(Segment,Parsed.delimiter)..Parsed.termination
      end
   end
   return output
end

-- a segment operation that will remove the extra pipe character
-- after the segment name
function oneoff.removeField(Parsed,i,Fields)
   --assumes that first segment is MSH
   local RemoveIndex = 2
   -- skip MSH delimiter definition
   if (i == 1) then RemoveIndex = 3 end
   table.remove(Fields,RemoveIndex)
end

-- a segment operation that will add an extra pipe character
-- after the segment name
function oneoff.addField(Parsed,i,Fields)
   local InsertIndex = 2
   -- skip MSH delimiter definition
   if (i == 1) then InsertIndex = 3 end
   table.insert(Fields,InsertIndex,'')
end

-- parses into segments and fields
-- return a table with the discovered delimiter and the 
-- segments+fields
function oneoff.parse( Data )
   local Delimiter = Data:sub(4,4);   
   local Segments =  string.split(Data,'r')
   local T = ''
   -- get Data terminator - append in genMsg()
   local i = -1
   while Data:sub(i,i)=='r' or Data:sub(i,i)=='n' do
      T = Data:sub(i,i)..T
      i=i-1
   end
   trace(T)
   -- remove any segments that are just white space
   local SegmentIndex = 1
   while SegmentIndex <= #Segments do
      if (Segments[SegmentIndex]:find('^%s*$')) then
         table.remove(Segments,SegmentIndex)
      else     
         SegmentIndex = SegmentIndex + 1
      end
   end
   --parse out the fields
   for SegmentIndex,Segment in pairs(Segments) do
      Segments[SegmentIndex] = string.split(Segment,Delimiter)
   end
   return { segments=Segments, delimiter=Delimiter, termination=T }
end

-- This function determines if a message should remove a field
-- or add a field after every segment name, by checking 
-- to see if the HL7 version number is in the right spot
-- and returning an adjustment operation
function oneoff.detectOneOffSegments( Parsed )
   local Fields = Parsed.segments[1];
   local VersionIndex = 12;
   local VerPattern = '2%.%d-'
   if (#Fields >= VersionIndex-1 and Fields[VersionIndex-1]:find(VerPattern)) then
         return oneoff.addField
   elseif (#Fields >= VersionIndex and Fields[VersionIndex]:find(VerPattern)) then
      return nil -- nothing to do
   elseif (#Fields >= VersionIndex+1 and Fields[VersionIndex+1]:find(VerPattern)) then
      return oneoff.removeField
   end   
   error('Cannot find version number')
end

-- A function that detects and applies the one off adjustment
-- as necessary
function oneoff.adjust(Input) 
   local Parsed = oneoff.parse(Input)
   local Operation = oneoff.detectOneOffSegments(Parsed)
   if (Operation) then
--      trace(tostring(Operation))
      return oneoff.genMsg(oneoff.forEachSegment(Parsed,Operation))
   end
   -- else nothing to do, return input
   return Input   
end

return oneoff

Leave A Comment?