HL7Script is used to perform a sequence of instructions based on the contents of one or more HL7 messages. It is primarily used to analyze or transform messages from an input file or wildcard and spit out some kind of output. That output can be collected/condensed information about the messages, or a modified set of the input HL7 messages. When used in conjunction with the HL7TransmitterService, it becomes a powerful integration engine able to modify and direct messages to and from numerous other systems.


Tip: Print this page to a PDF for a local copy of the reference.

Table of Contents

Script Syntax Conventions

HL7Script script files are plain text files. They contain one or more commands to perform on a series of input HL7 messages.

A few lines of a typical script might look something like this:

SET NEWID="X" + PID.3.1 ;Stick an X on the front of the MRN
HL7 SET PID.3.1=V@NEWID ;Replace the MRN in the output HL7 message 
HL7 OUTPUT ;Write the output HL7 message to disk

HL7 Data

To reference a value from the input HL7 message, use the following key syntax:

SID[#q].f[~r][.c[.s]]

Where:
SID = 3-letter segment id (e.g. MSH, PID, etc.)
q = segment seQuence
f = Field index
r = Repetition index
c = Component index
s = Subcomponent index

Examples: PID.3.1, PV1.44, OBX#2.5~3.1

A field index is always required, except in a few situations where only a segment reference is needed (see LOOP, HL7 Commands). All non-provided index values default to 1.

Field index zero references the segment ID (e.g. PID.0 = "PID").

If there is more than one of a given type of segment, the segment sequence is used to identify which one you want. Sequence 1 is the segment that appears first. For example, if there were three DG1 segments you would refer to the diagnosis codes as DG1#1.3.1, DG1#2.3.1 and DG1#3.3.1. Without a sequence index, the first such segment is assumed (DG1#1.3.1 is equivalent to DG1.3.1).


Note: The script supports Named Fields in field keys if a Named Fields file is selected in the program preferences or script. Named Fields let you use field keys with "friendly" names rather than (or in addition to) numeric indexes and lets you validate message data against field definitions (see HL7Viewer).

Strings

"foo" = A string literal value is enclosed in double quotes. If you require an actual quote character in the string, double it. For example, to specify a lone quote character, you would use """".

+ = The plus sign is the string concatenation operator. Whitespace around the plus is allowed.

;Example: Create a formatted name as "Last, First Middle"
SET FULLNAME=PID.5.1 + ", " + PID.5.2 + " " + PID.5.3

Comments and Whitespace

Single-line comments are specified with a leading semicolon (;). Semicolon end-of-line comments are also supported.

Block comments are supported using /* and */ pairs, and may appear at the end of a line (only one to a line). Block comments may be nested.

Whitespace is ignored, so feel free to leave blank lines and indent for clarity.

Return to Top

Pre-Processing

A script may optionally start with a pre-processing section. This section is used to initialize global variables, set up translation tables, and create user-defined procedures. It must appear at the very start of the script, or be preceded only by comments. The section must be surrounded by <PRE> and </PRE> on lines by themselves. This section is processed only when the script is first loaded or when reset.

; Example Pre-Processing Section
<PRE>
  TRANSLATION=tablename
    "input"="output"
    "FOO"="BAR"
  ENDTRANS
  PROCEDURE=procname
    INC _CALLCOUNT
    LOG "You have called this procedure " + V@_CALLCOUNT + " times."
  ENDPROC
  _GlobalVar="value"
  __Debug="1"
</PRE>

Translation tables are used in message processing to translate one value to another (see the TRANSLATE function). Start the entry with TRANSLATION, an equal sign, and the name of the translation table. The translation table name may or may not be quoted. You may have as many translation tables defined as you need as long as each table name is unique. The input and output values should be quoted as they are considered data. Translation table lookups are case-sensitive. Each translation table must be finished with ENDTRANS on a line by itself.

User-defined procedures can also be defined in the pre-processing section. The procedure name may or may not be quoted. All lines between the PROCEDURE and ENDPROC lines become the body of the procedure. The procedure can be called at any point in the main script using the "CALL procname" syntax. Basically, procedures are an easy way of repeating the same block of lines in more than one place in the script.

Any other lines in the pre-processing section are considered to be variable assignments. These have the following format:

_GlobalVar="Value"

Global variables start with an underscore so that the program knows not to reset them between messages. Global variables persist for the life of the script or until deliberately reset. Initializing a non-global variable in the pre-processing section would be pointless because the variable would be cleared as soon as the first input message was loaded. See the section on variables for more information.

Use of the SET keyword is not needed when assigning variables in the pre-processing section, but it is allowed and silently discarded. Its use will display a warning during script validation.

Return to Top

Post-Processing

The post-processing section is also optional. If present, it must be located at the end of the script file and be surrounded by <POST> and </POST> on lines by themselves. It is performed after all messages from the input have been processed (either per file or per interval, depending on options) and is usually used to finish things up or spit out some collected information from the messages.

<POST>
  LOGGLOBALS
</POST>

There are options to either run or skip post-processing if the script is aborted. If HL7Script processing is cancelled by the user, the user will be prompted for the choice.

The syntax of the post-processing section is not limited like the pre-processing section. All regular script commands are available in post-processing, except that the input HL7 message is undefined and must not be referenced. Global variables are reset after post-processing, even if there is no post-processing code.

Everything between the pre-processing and post-processing sections is the "main" script, and is run for each message.

Return to Top

$INCLUDE

A line that starts with $INCLUDE and is followed by a script filename will include the lines of the referenced script file into the calling script at that location.

If the script filename does not include a path, the path of the main script file is assumed. The filename must be a literal value because included scripts are loaded prior to pre-processing -- you cannot use any variables or script logic to create the filename.

Included scripts may not contain pre-processing or post-processing sections. Included scripts can be nested, and circular references are detected/prevented.

$INCLUDE IncludeMe.h7s

If an error message is logged for a line that comes from an included file, the line number will be specially formatted to indicate exactly where the line originated.

Line 5-4: Circular include reference: $INCLUDE IncludeMe.h7s

In the above example, it can be seen that on line 5 of the main script, a file was included. On line 4 of the included script, an error was raised.

Return to Top

Variables

SET

To create or assign a variable, use the SET command followed by the variable name, an equal sign, and the value. The value assigned can be a string literal, an HL7 data value, another variable, or a concatenation of multiple values.

Variable names are alphanumeric and case-insensitive. If you refer to a variable name that does not exist you simply get a blank value, so check your spelling! Once set, a variable exists until the end of the current message is reached or until it is cleared.

Global variables are indicated with a leading underscore and persist until post-processing is completed or until deliberately cleared.

SET FOO="bar"
SET NEWID = PID.8+"."+PID.9
SET MSGID = Z@"NOW"

To clear or undefine a variable, just set it to blank:

SET NEWID=""

To reference a variable, use the V@ modifier followed by the unquoted variable name:

OUTPUT V@NEWID

INC and DEC

The INC command increments a variable by adding one to it (by default), assuming it to be numeric. If the variable doesn't yet exist or is not numeric, it is set to the increment value.

DEC works just like INC but decrements the value with subtraction instead of addition. If the value doesn't exist or is non-numeric, it is defaulted to zero and then decremented.

INC FOO ;Add 1 to FOO
DEC BAR ;Subtract 1 from BAR

An optional value can be provided after an equal sign, and this value can be negative. It can be an unquoted literal number or a data value.

DEC FOO=2 ;Decrement FOO by 2
INC FOO=V@FOO ;Double the value of FOO

Dynamic Variable Names

You may create variables with dynamic names that come from data. For example, if you wanted to find out how many messages there were in a file for each given patient id, you could do something like this:

INC "_"+PID.3.1

If the input contained 7 messages for a patient with an id of "12345", the "_12345" global variable would have a value of "7" at the end of the input.

Built-In Variables

There are a number of "built-in" variables available to the script. These built-in variables all start with double underscores, like "super global" variables. These are used to get information about the current script, environment, and settings. Some are read-only, and some may be set to affect script behavior.

Variables are strings unless otherwise noted. Boolean values use "0" and "1" for False and True, respectively.

Read-Only Variables

__Aborted
Boolean. Could be used in post-processing to know if the script was aborted or not.
__CurrentPath
The current directory with trailing backslash.
__InFile
The full name of the input file that is being processed.
__InName
Just the filename (without path) of the input file.
__InPath
The path (with trailing backslash) of the input file.
__InputMsgStr
The input HL7 message's MessageStr property with standard 0x0D (CR) segment terminators.
__InputMsgText
The input HL7 message's MessageText property with 0x0D0A (CRLF) at the end of each segment.
__LastSeg
Numeric. The 0-based index of the last segment in the current HL7 message. This will always be one less than the value of __SegCount.
__LastVar
Numeric. The 0-based index of the last user-defined variable. This will always be one less than the value of __VarCount.
__OutFile
The full name of the primary output file. May be blank.
__OutPath
The path of the primary output file. Blank if __OutFile is blank.
__OutputMsgStr
The output HL7 message's MessageStr property with standard 0x0D (CR) segment terminators.
__OutputMsgText
The output HL7 message's MessageText property with 0x0D0A (CRLF) at the end of each segment.
__ScriptFile
The full name of the current script file.
__ScriptPath
The path of the current script file.
__SegCount
Numeric. The count of segments in the current input HL7 message.
__VarCount
Numeric. The count of all currently defined variables, both global and non-global.

Read-Write Variables

Built-in variables may not be INCremented or DECremented.

__Anonymizer
Gets/sets the name of the anonymizer definition file for use in the HL7 ANONYMIZE command. Changing the definition is persistent, even across resets; it will remain assigned until changed to something else or the service is restarted. Setting it to blank will unassign an anonymizer defintion. To reset the anonymizer's persisted data (if not using a DataStore file), re-assign the variable. It can be done in pre-processing or within the main script.
; Set the anonymizer up just once:
IF V@__Anonymizer==""
  SET __Anonymizer="D:\HL7\Generic.anon.ini"
END
__AutoADD
Gets/sets the boolean AutoADD message property on the Input and Output HL7 messages. When True, ADD segments are automatically combined when a message is loaded. This works regardless of the Compact and ForceField property settings. The default is False unless set otherwise in the program GUI preferences.
__Compact
Gets/sets the boolean Compact message property on the Input and Output HL7 messages. Strips any empty trailing field, component, subcomponent or repetition separators from the message when True. The default is True or set by preferences.
__Debug
Boolean, default is False. When True, each script line is logged as it is evaluated in addition to other helpful information about comparisons and flow control.
__DebugData
Logs every expression evaluated as data. Boolean, False by default.
__Epsilon
When comparing floating point numbers for equality, Epsilon can be set to a non-zero value (like 0.00001) that allows the two values to differ ever so slightly and still be considered equal. Numeric, default=0.
__FileEOL
The end-of-line character(s) used for FILE output. String, default=CRLF (0x0D0A).
Typically, this would be set by using the H@ data modifier on a hex string value, such as SET __FileEOL=H@"0D0A".
__ForceField
Gets/sets the boolean ForceField message property on the Input and Output HL7 messages. When True, each segment always has a trailing field separator. The default is False or set by preferences.
__MaxLoops
The maximum number of loop iterations allowed. Numeric, default=1000.
This prevents hanging the program due to endless loops. An exception is thrown if a loop exceeds this count.
__MinimalHex
Gets/sets the boolean MinimalHex message property on the Input and Output HL7 messages. When True, as little hex escaping as possible is done. When False, an entire value is hex encoded if any part of it requires encoding. Boolean, default is True or set in preferences.
__NamedFields
Gets/sets the name of the Named Fields file for use in parsing named field keys and the VALIDATE script function. Changing the Named Fields file is persistent, even across resets; it will remain assigned until changed to something else or the service is restarted. Setting it to blank will unassign a Named Fields file.
__Strict
Gets/sets the boolean Strict message property on the Input and Output HL7 messages. When True, message syntax during parsing is more strictly enforced. Defaults to True or is set in preferences.
__StrictNum
The boolean __StrictNum setting determines what happens when converting a string to a number when the string isn't numeric (blank, "foo", HL7 null, etc.). When True (the default) an exception is raised, halting execution of the script. If set to False (0), the converted value will instead be silently defaulted to zero and script processing will continue. See also: The INTDEF and FLOATDEF script functions.
__TZLocal
The local timezone in +/-HHMM format. Defaults to whatever Windows is currently set to.
__TZSender
The current input message's default timezone in +/-HHMM format. The value is reevaluated for each input message. If a message contains a timezone in MSH.7, that value is used. Otherwise, it is set to __TZSenderDefault.
__TZSenderDefault
The default timezone to use for all input messages that don't provide their own default in MSH.7. Defaults to whatever Windows is currently set to.

SAVEVARS and LOADVARS

Regular variables last only until the end of the script, and global variables are reset after post-processing. If you need some variables to persist between resets, you can use the SAVEVARS and LOADVARS commands. These commands save and load variables using simple "name=value" text files.

SAVEVARS can be called at any time during the script, but would most likely be called in post-processing. Follow the command with a space and the name of the file you want the variables stored in, another space and a comma-separated list of variable names to save.

The comma-separated variable list can also contain asterisk wildcards. A single asterisk "*" will save all currently defined variables. An underscore followed by an asterisk "_*" will save all global variables. Any other text followed by an asterisk will save all existing variables with a name that starts with that bit of text, like "_FOO*". The asterisk must always appear last in a wildcard.

If you specify a non-wildcard variable name in the list that does not exist, it will be added to the saved file with the special value "$UNDEFINED$". When loaded, that variable will be set to blank and undefined if it exists.

The filename and variable list can be unquoted literals (if they don't contain any spaces), quoted strings, or simple variable references such as V@VARFILE and V@VARLIST. More complex expressions are not supported.

;Examples
SET VARFILE="C:\Data\Vars.txt"
SAVEVARS V@VARFILE "_KEEP*,_GRANDTOTAL"
SAVEVARS "C:\Data\All Globals.txt" "_*"

LOADVARS can also be called anywhere, but would most likely be used in pre-processing. It expects just a filename, and will load any variables stored in the file. When called from pre-processing, put an equal sign between LOADVARS and the filename. In the main script, just use a space.

If the file referenced by LOADVARS does not exist, a log entry will be made but processing will continue normally; it is not considered an error.

;Pre-Processing example
<PRE>
  LOADVARS="C:\Data\Vars.txt"
</PRE>

;Main script example
LOADVARS "C:\Data\Vars.txt"

Return to Top

Data Modifiers

Modifiers are placed in front of other expressions (literals or data) and serve to change it in some way, like a little inline function with a single argument. Modifiers are a single character followed by an at-sign (@). More than one modifier may be used in a row. If more than one is attached to a value, the modifiers are applied right-to-left, as in the following example:

;If PID.5.1 is null change it to blank (B@), then upper-case the value (U@).
SET X=U@B@PID.5.1

A modifier applies only to the value it is directly attached to. In other words, a modifier does not cross a concatenation boundary (+). In the following example, only "foo" would be made upper-case, not "bar", resulting in the variable X being set to "FOObar":

SET X=U@"foo" + "bar" ; Result is "FOObar"

You can use parentheses to extend what a modifier applies to. Place an open paren directly after the at-sign (no spaces!) and the closing paren after the last value you wish to affect. In the following example, the entire value would be made upper-case, resulting in X being set to "FOOBAR":

SET X=U@("foo" + "bar") ; Result is "FOOBAR"

All the modifiers that work on date/time values (D@, M@, S@, T@, Z@) expect the value to be an HL7 date/time value (HL7 data type DT, TM or TS) or the quoted literal "NOW" for the current system time. Blank or null input is unchanged by the date/time modifiers.

Here is the full list of available modifiers in alphabetical order:

A@ - ASCII
The Delphi-style ASCII notation (e.g. "#13#10") that follows is converted into a string.
B@ - Blank-if-Null
If the value is HL7 null (""), change it to a blank string.
C@ - Call function
Calls a special built-in script function. The value that C@ is attached to must be a string in the format of a delimited list like so:

FUNCTION,ARG1[,ARG2...]

The first non-alphanumeric character in the string becomes the list delimiter, which comes in handy when your data may contain commas. The first value is the function name, and any subsequent values are the arguments to that function. All functions require at least one argument, and some require more. See the Script Functions section for a complete list of supported script functions. Examples:
SET DIFF=C@("MATH,-," + ZA1.3 + "," + ZA1.4) ; Subtract ZA1.4 from ZA1.3
SET NEWID=C@("PADL,10," + PV1.3 + ",0") ; Pad the MRN with leading zeros
SET DIAGTEXT=C@("LEFT|100|" + DG1.3.2) ; First 100 characters of data that may contain commas
D@ - Date
Formats a date/time value as a date only. Example: D@"20150409153321" becomes "20150409"
E@ - Enquote
Enquotes the value in single quotes. Any internal quotes are doubled. Useful for SQL queries. Example: E@"O'Malley" becomes 'O''Malley'.
F@ - FieldStr
Returns the entire FieldStr for the requested field key. The FieldStr is the entire field as it appears in the message, with all separators and escaping still there. For example, F@MSH.9 would get you something like "ADT^A08".
G@ - SegmentStr
Returns the entire SegmentStr for the requested segment key, with all separators and escaping intact.
H@ - Hex
The hex value that follows is converted into a string. Case does not matter.
SET __FileEOL=H@"0D0A"
K@ - Key
The string expression following K@ is evaluated as field key, retrieving the specified value from the current input HL7 message.
SET PATIENTTYPE=K@("PV1#"+V@COUNTER+".18.1")
L@ - Lower-case
Change the data to lower-case.
M@ - Minutes
Converts the time portion of a date/time value into an integer number of minutes since midnight. Could be useful for appointment durations in SCH or AIS segments.
N@ - Null-if-Blank
If the value is blank, change it to an HL7 null ("").
O@ - Output key
Works like K@ where it expects a field key, but gets the data from the output HL7 message rather than the input message.
P@ - Prune
"Prune" the value by trimming leading and trailing spaces. Equivalent to the TRIM script function.
R@ - RepetitionStr
Returns the entire RepetitionStr for the requested field key. For example, R@PID.3~1 would get you the first patient identifier like "12345^^MRN".
S@ - Seconds
Formats a date/time value as a timestamp with a precision of seconds. Example: S@"NOW" -> "20150409153345"
T@ - Time
Formats a date/time value as a time-only value with a precision of seconds. Example: T@"20150416081422.3250" -> "081422"
U@ - Upper-case
Change the data to upper-case.
V@ - Variable
Precedes a variable name to retrieve the variable's value. The variable name can be an unquoted literal variable name or a data value. Examples: V@FOO, V@("_" + PID.3.1)
Z@ - Milliseconds
Formats a date/time value as a timestamp with millisecond precision. Example: Z@"NOW" -> "20150409153345.5210"
9@ - Digits
Removes all characters except numeric digits. Equivalent to the DIGITS script function.
0@ - Noop
Zero is a "noop" (no operation) modifier and does nothing to the data. Left over from testing, it might come in handy someday...

Return to Top

Script Flow Control

The main script is processed from top to bottom for each input message. Within the script, you can branch and loop based on what is contained in the message. All such flow control is done using IF and LOOP blocks.

IF/LOOP = Starts a block. The keyword is followed by the condition(s) or loop iterator of the block (see below).

ELSE = Starts the "else" part of an IF block. Not valid in a LOOP block.

END = Ends the block started by the nearest IF/LOOP line. Blocks can be nested with only a few restrictions.

All IF/LOOP/ELSE/END statements must appear on a line by themselves.

IF Blocks

IF blocks work like they do in most programming languages. The expression(s) following IF are evaluated for a true/false answer. If true, the block is processed until an ELSE or END is encountered. If false, the statements between ELSE and END are executed, if any. IF blocks may be nested without restrictions.

The expressions are joined logically by AND, OR and XOR. You may also prefix expressions with NOT to flip the true/false value. Parentheses are fully supported.

; This block only sets the discharge date for ADT^A03 messages
IF MSH.9.1=="ADT" AND MSH.9.2=="A03"
  HL7 SET PV1.45.1 S@"NOW"
END

In the absence of parentheses, multiple logical operators are evaluated from left to right. The unary NOT operator is applied to the expression on its right before any of the other logical operators are evaluated. The expression "1 OR NOT 0 AND 1" evaluates as if it were written as "(1 OR (NOT 0)) AND 1".

The simplest expressions are 0 or 1 - false and true as literal values. However, most expressions are going to be comparisons against the HL7 data. You may specify any of a number of comparison operators (all of which are two characters in length for ease of parsing):


Comparison Operators
==equal
<>not equal
>>greater than
<<less than
>=greater than or equal
<=less than or equal
$$contains substring
*=appears in list
~=starts with
=~ends with
~~LIKE pattern matching
#=numerically equal
#>numerically greater than
#<numerically less than

Whitespace around the operator is supported. The following three examples are all logically identical:

IF PID.3<>""
IF PID.3 <> ""
IF NOT PID.3 == ""

All of the string comparisons are case-sensitive. If you want to do a case-insensitive match, use the "U@" or "L@" data modifiers on both sides to make sure the case of the strings are equal.

Nulls ("") are not considered equal to blanks in these comparisons. If you want to test blanks against nulls, prefix one or both data elements with one of the "B@" Blank-if-Null or "N@" Null-if-Blank modifiers.

The numeric comparisons convert the left and right values to numbers for the comparison. If either value contains a decimal point, the numbers are compared as floating point numbers. When comparing floating point numbers for equality with the #= operator, the __Epsilon built-in variable can be set to allow slight differences to still be considered equal. If a value is not a valid number, an exception is raised (by default, see __StrictNum). The INTDEF and FLOATDEF script functions can be used to give a valid default value to possibly invalid numeric data.

The $$ "contains" operator can also be thought of as meaning "has $ub$tring". If the value on the left contains the value on the right, the expression evaluates to true.

The *= "appears in list" operator compares the value on the left to a string on the right that contains a delimited list of values. It returns true if the left side value is one of the values in the list. The first character in the list string indicates the delimiter value (it must be non-alphanumeric) so you can use something besides a comma if needed.

;Do something if PV1.10 is "FOO", "BAR", "AS IF" or "MEH"
IF PV1.10 *= ",FOO,BAR,AS IF,MEH"
  ;...
END

The ~~ LIKE pattern matching operator compares the value on the left to the pattern on the right and returns true if they match. The pattern uses the default "%" for any-character matching, "_" for single-character matching, and "\" as the escape character. A blank pattern will always return a false result. See the llLike.TLikePattern class for more info.

"AnyKey" Matching

Field keys on the left side of any IF statement comparison may have one of the indexes replaced by an asterisk (*) in order to compare against values in any matching message element.

The following example will evaluate to True if any DG1 segment has the letter "A" in field 6:

IF DG1#*.6 == "A"
  LOG "There is an admitting diagnosis in this message!"
END

The asterisk can be in any index position, but only one asterisk may appear in a key at a time. The most common uses would be in the segment sequence or repetition index positions. It is worth noting again that only field keys on the left side of the comparison operator are checked for asterisks. It must be a lone field key and must not include any quoted literals or concatenation. Modifiers are allowed, like U@ or L@.

This feature was developed primarily for use by HL7Viewer's filtering and Find in Files features, but it could save you a line or two of code in a script.

An AnyKey match will keep incrementing the number in the asterisk index and doing comparisons until the key is found not to exist three consecutive times. Only then does it assume there is no more data to check and gives up. When doing debug logging, you may notice the checking of iterations beyond the end of the actual data due to this three-in-a-row logic.

Each comparison is checked separately. Trying to do something like the following with two AnyKey matches in the same IF statement does not work like you might think:

IF DG1#*.6 == "A" AND DG1#*.15 == "1"
  LOG "There is a primary admitting diagnosis in this message!" ; Not necessarily true!
END

It could not be assumed that the DG1.6 "A" and DG1.15 "1" were actually found in the same segment. To do that properly, you would want to use a LOOP...

LOOP Blocks

LOOP blocks will be repeated as long as the identifier after the LOOP keyword exists or the expression evaluates to True. Single question marks (?) within the block are replaced by the 1-based numeric iterator index on each loop. Double question marks (??) are replaced with the 0-based index.

To iterate over a certain type of segment, use a 3-character segment ID iterator: LOOP DG1

The following example is the correct way to find out if there is a primary admitting diagnosis in a message, as compared to the AnyKey example above that would not always work as desired:

LOOP DG1
  IF DG1#?.6 == "A" AND DG1#?.15 == "1"
    LOG "There is a primary admitting diagnosis in this message!" ; This is true!
  END
END

To iterate over a repeating field, use a field key iterator: LOOP PID#1.3

If a field repetition loop will be nested (outer or inner) you must include the segment sequence index (e.g. #1) in the iterator.

; This example outputs each id of the repeating patient identifier list:
LOOP PID#1.3
  OUTPUT PID#1.3~?.1
END

To loop through all the segments of the input message, use LOOP SEGMENTS.

If you want only a subset of all segments, you may specify a numeric range in the following format: LOOP SEGMENTS [start[-end]]

The first segment is segment zero (usually MSH). If no end segment is specified, the loop will process to the last segment. The segment range can come from data, for example:

LOOP SEGMENTS V@FIRSTSEG+"-"+V@__LastSeg

Use ### as the replacement value for the current segment within a SEGMENTS loop. The regular ? and ?? replacements are also available.

; Output every segment in the message except "Z" segments
HL7 CLEAR
LOOP SEGMENTS
  IF C@("LEFT,1,"+###.0)<>"Z"
    HL7 COPYSEG ###
  END
END
HL7 OUTPUT

LOOP SEGMENTS blocks may not be nested, but other loop types can. In nested loops, only the original iterator from the outer block (e.g. IN1#?) is replaced inside the inner blocks. Inner loop question marks receive their value from the loop block they are defined in.

A variable loop uses the variable name prefixed with an at-sign (@): LOOP @FOO

The loop continues as long as the variable remains defined. No question mark replacements are done inside a variable loop.

SET FOO="0"
LOOP @FOO
  LOG "FOO=" + V@FOO
  INC FOO
  IF V@FOO #> "3"
    ; Setting the variable to blank will end the loop
    SET FOO="" 
  END
END

An expression loop will repeat so long as the expression evaluates to True. The expression following LOOP is the same as one following an IF statement, and is identified by the presence of one or more of the comparison operators. Like variable loops, no question mark replacements are done inside expression loops.

SET FOO = "0"
LOOP V@FOO #< "3"
  LOG "FOO=" + V@FOO
  INC FOO
END

To prevent runaway scripts, there is a maximum loop count property. If a loop exceeds this number of iterations, an exception will be raised. The default value is 1000. This value can be read/written in the script using the __MaxLoops built-in variable.

BREAK and CONTINUE

BREAK is used to break out of loops early. It can be followed by a number to indicate how many levels of LOOP nesting should be broken out of. If no value is specified, one (1) is assumed. Trivia: BREAK 0 is effectively just a jump to the nearest END statement.

CONTINUE is used to skip the rest of the current loop iteration and move on to the next without breaking the loop completely.

BREAK and CONTINUE must appear on lines by themselves.

SET X="0"
LOOP @X
  INC X
  IF V@X #< "4"
    CONTINUE
  END
  LOG "This line is only reached when X is 4 or 5"
  IF V@X #= "5"
    BREAK
  END
  ; CONTINUE takes us here, but the loop keeps running
END
; BREAK leaves us here - outside the nearest LOOP..END

QUIT, ABORT, and ERROR

You can specify QUIT at any point in the script to stop processing the script for the current message and move on to the next.

IF B@PV1.19==""
  QUIT
END

ABORT is similar, but stops processing ALL messages from the current input file or wildcard. It will also skip post-processing unless you choose to allow it.

ERROR works similarly to ABORT but actually raises an exception. The text of the error comes from the remainder of the line - ERROR must be followed by a space and an expression. An ERROR will always cause post-processing to be skipped.

IF B@PID.3==""
  ERROR "No MRN provided"
END

CALL

You may call a user-defined procedure at any point in the script using the CALL command followed by the name of the procedure as defined in the pre-processing section. The procedure name can be an unquoted literal value or data. The lines of the procedure are run as if they were inserted into the script at that point and then the script continues where it left off.

Calling a procedure inside a loop does not perform any question mark or segment substitution on the lines of the procedure.

User-defined procedures accept no arguments and return no values, but they do have full read and write access to all variables when they are called. Variables that are set or modified during the procedure continue to exist after it finishes and are available to the rest of the script.

Here is an example of using variables within a procedure to mimic a function that accepts arguments and returns a value:

<PRE>
  PROCEDURE=TZEXTRACT
    /* Takes a date/time value and extracts the timezone if present.
     * Set TZX_DATETIME as the input date/time value. This variable will be 
     * updated by having the timezone removed from it.
     * The TZX_TZ variable will contain the extracted timezone. 
     */
    SET TZX=C@("POS,+," + V@TZX_DATETIME) ;Is there a plus?
    IF V@TZX == "0" ;If not, how about a minus?
      SET TZX=C@("POS,-," + V@TZX_DATETIME)
    END
    IF V@TZX == "0"
      SET TZX_TZ="" ;The input does not contain a timezone
    ELSE
      SET TZX_TZ=C@("SUBSTR," + V@TZX_DATETIME + "," + V@TZX)
      DEC TZX
      SET TZX_DATETIME=C@("SUBSTR," + V@TZX_DATETIME + ",1," + V@TZX)
    END
  ENDPROC
</PRE> 

SET TZX_DATETIME=PV1.45 ;"20150427080000-0700"
CALL TZEXTRACT
LOG V@TZX_DATETIME      ;"20150427080000"
LOG V@TZX_TZ            ;"-0700"

Return to Top

OUTPUT and FILE

The expression following OUTPUT is evaluated and written to the output file. The end-of-line terminator used in the output file is determined by program settings.

;Write the value of FOO to the output file
OUTPUT "FOO="+V@FOO

If the output file is configured in the program GUI (the primary output file), that is the file used and the end-of-line setting comes from the program settings. This file remains open for the duration of the input and is closed only after post-processing (if any). This is more efficient than repeatedly opening/closing the file.

The primary output file is available only in the HL7Script GUI application. If you are using HL7ScriptService, or your output needs are more dynamic than this (i.e. you need to write to more than one file), you may control output files from within the script by using FILE commands. Files opened in this manner stay open only until the end of the current input message and are not available in post-processing (unless re-opened there). The end-of-line character(s) for FILE output is controlled by the __FileEOL built-in variable. Only one FILE can be open at a time.

You can use a combination of primary and FILE output. If a script FILE is open, it will be the target of any OUTPUT commands. If no script FILE is open, the program will fall back to writing to the primary output file. If no output file is available at all, any OUTPUT commands will instead go to the log. You can force output to go to the primary output file when FILE output is in use by using the OUTPUTPRIMARY or HL7 OUTPUTPRIMARY command variations. Using the OUTPUTPRIMARY variations when there is no primary output file open will result in your output going to the log.

None of the following commands affect the primary output file at all.

FILE CLOSE
Closes the currently open output file.
FILE APPEND filename
Closes any currently open file and then opens the specified output file. If the file already exists it will be appended to.
FILE REWRITE filename
Closes any currently open file and then opens the specified output file. If the file already exists it will be truncated/overwritten.
FILE DELETE [filename]
Deletes the specified file. If no filename is supplied, the currently open output file is closed and then deleted.
FILE COPY|MOVE|RENAME source=target
Copies/Moves/Renames the source file to the target filename. If the source file is currently open, it will be closed. COPY and MOVE will overwrite the target file if it exists.

If no explicit path is provided in the FILE filenames, the current directory is assumed. This is the current directory of the application, not necessarily the input or script file. The __CurrentPath, __InPath, __OutPath and __ScriptPath built-in variables may be of use in constructing filenames.

If a FILE command fails, an exception will be raised.

Return to Top

HL7 Commands

Often, a script will be used to output some or all of a set of input messages in a slightly different format. The script has an input HL7 message and an output HL7 message. To work with the output HL7 message, use HL7 commands.

Syntax: HL7 COMMAND [OPTIONS]

Commands available:

HL7 ADDSEG segmentString
Adds a segment to the output message. The data that follows the ADDSEG command is treated as a SegmentStr. It can be as small as just the segment ID or as detailed as needed. Example: HL7 ADDSEG G@PID
HL7 ANONYMIZE
Anonymizes the output message using the currently loaded anonymizer definition, controlled by the __Anonymizer built-in variable. If there is no definition loaded, an exception will be raised.
HL7 APPEND[TEXT] filename
Adds the output message to a file using the message's AppendToFile method. If the file does not yet exist, it will be created. If APPENDTEXT is used, the message will be saved with CRLF line endings. See also: HL7 SAVE[TEXT]
HL7 CLEAR
Clears the output HL7 message. The output message is not automatically cleared between messages in case you need to combine multiple messages into one. If you have unexpected data in your output message, you probably forgot to CLEAR it.
HL7 CLEARSEG index|segmentKey
Clears the segment specified by the 0-based numeric index or segment key in the output message, leaving only the segment ID (and encoding characters if a header segment). Example: HL7 CLEARSEG AL1#2
HL7 COMBINEADD
Combines any ADD segments in the message. The Compact and ForceField message properties must be False for this to work reliably because those options modify trailing separators.
HL7 COPYINPUT
Clears the output message and replaces it with a copy of the input message.
HL7 COPYSEG index|segmentKey
Copies the requested segment from the input message and adds it to the output message. If the specified segment does not exist in the input message, a blank segment of that type is added.
HL7 DELREP fieldKey
Deletes the specified field repetition from the output message. Example: HL7 DELREP PID.3~2
HL7 DELSEG index|segmentKey
Deletes the specified segment from the output message.
HL7 FORCECOMPACT
Compacts the data (removes extra trailing separators) in the output HL7 message even when the Compact property (__Compact) is turned off.
HL7 INSERTSEG index segmentString
Like HL7 ADDSEG, but you specify the 0-based index where you want the new segment inserted into the output message.
HL7 LOAD filename
Loads the output HL7 message from a file using the message's LoadFromFile method. Handy if you are building a message out of multiple non-sequential input messages.
HL7 OUTPUT[PRIMARY]
Writes the output HL7 message to the active [or primary] output file, one segment per line. The line terminator of the output file is determined by program settings.
HL7 [I]REPLACE level old=new
Replaces values in the output message with new values. The replacement is case sensitive unless IREPLACE is specified to make it case-insensitive. You must specify at what level you wish to replace the data using one of the following values (which may be shortened to three characters if desired):
MESSAGE
Replaces old with new at the MessageStr level.
SEGMENT
Replaces old with new at the SegmentStr level.
FIELD
Replaces old with new at the FieldStr level.
SUBCOMPONENT
Replaces old with new at the subcomponent level. This level (or KEY) should be used for most replacements, as all data is raw and unescaped.
KEY
old is given as an HL7 key. If a specific segment sequence and/or field repetition are specified, only those specific instances will be replaced with the new value. Otherwise, all matching segments and repetitions are replaced. Only existing values are changed - to add values, use HL7 SET.

Examples:
HL7 REPLACE KEY PID.3="12345" - Replaces any PID.3, like PID#?.3~?.1.1.
HL7 REPLACE KEY PID#1.3="12345" - Replaces any PID.3 repetition in the first PID segment only.
HL7 REPLACE KEY PID.3~1="12345" - Replaces only PID.3~1 in any PID segment.
HL7 REPLACE KEY PID#1.3~1="12345" - Replaces only PID#1.3~1.1.1 (use SET!)
HL7 REPLACESEG index|segmentKey segmentString
Replaces the segment in the output message specified by the 0-based numeric index or segment key with the provided segment string. An exception will be raised if the numeric index is out of range or the segment key resolves to a segment that does not exist.
HL7 SAVE[TEXT] filename
Writes the output message to a file using the message's SaveToFile method. If the file exists, it will be overwritten/replaced. If SAVETEXT is used, the file will be saved with CRLF line endings. See also: HL7 APPEND[TEXT]
HL7 SET fieldKey=data
Assigns the data to the specified field key in the output HL7. If you use asterisks in place of the segment sequence and/or field repetition index, all such instances will be set. Sort of the opposite of HL7 REPLACE, which replaces all instances by default.

Examples:
HL7 SET PV1.2=PID.18.1 - Assigns PID.18.1 from input to PV1#1.2~1.1.1 in output.
HL7 SET IN1#*.3="SELF" - Sets IN1.3.1.1 in any IN1 segment to "SELF".
HL7 SET PID.3~*="12345" - Sets all repetitions of PID#1.3.1.1 to "12345".

If the field does not exist, a single repetition is added/set to the value. If you only want to change existing values, use HL7 REPLACE KEY.
HL7 SPLITADD FIELDS|LENGTH|SEPARATOR=Max
Splits long segments into ADD segments. The FIELDS option splits by the Max number of fields. The LENGTH option splits by Max length. SEPARATOR splits by Max length at the nearest separator <= Max. Only the first character of the split type is required. The Compact and ForceField message properties must be False for this to work reliably.

Return to Top

LOG Commands

LOG works like OUTPUT, but writes to the log file (and/or screen) with a timestamp. The format of the timestamp is determined by program settings.

If you log an empty string (i.e. LOG "") a blank line will be added to the log without a timestamp for formatting purposes.

LOGWHEN is a way to do some conditional or trace logging without everything that goes along with the __Debug setting. The LOGWHEN command works just like LOG, but the second word on the line is an unquoted variable name. The log entry will only be made if that variable is currently defined (not blank). You can also use a very simple expression instead of a variable name, but it cannot contain any spaces or concatenation, and must resolve to the name of a variable in order to work.

LOG "This will always make it to the log."
SET EXTRA=""
LOGWHEN EXTRA "This will not get logged."
SET EXTRA="1"
LOGWHEN EXTRA "Extra logging is turned on!"
SET REFVAR="EXTRA"
LOGWHEN V@REFVAR "See what I did there? This also gets logged."

The following commands are also available to write formatted information to the log file:

LOGVARS
Sorts the variables by name and then writes them all to the log in "Name=Value" format, one per line.
LOGGLOBALS [NOUNDERSCORE]
Does the same, but only writes the global variables. If the NOUNDERSCORE option is included, the leading underscore on the variable names will be omitted.
LOGSCRIPT
Dumps the entire script to the log with line numbers. Blank lines, comments, and whitespace will have been removed from the script during pre-processing.
LOGBLOCK
Dumps the lines of the current IF/LOOP block being processed. The IF/LOOP and END lines themselves are not included.
LOGTRANSLATIONS
Dumps any translation tables to the log.

When using HL7ScriptService, all logging done in the script is considered to be at the default "Info" logging level.

Return to Top

Script Functions

These are the functions available for the Call Script Function data modifier (C@).

The functions that take a datetime argument expect it to be in HL7 format or the string constant "NOW" for the current system date/time. Blank or null input is interpreted as a "zero" date (1899-12-30 00:00:00).

Arguments shown in [square brackets] are optional. A default value for a missing argument is shown with an equal sign and value after the name.

Notice that many of the numeric functions include an optional "modifier" argument. This is a convenience to save you a separate call to MATH or INC. The modifier can be negative.

CLEAN,string[,allowedchars=@@##]
Returns the string stripped of any characters not in the allowed character list. If allowedchars is not provided, it defaults to alphanumeric. See llStrings.CharsOnly for more information.
COALESCE,string1,string2[,string3...]
Returns the first non-null argument. If all of the arguments are null, the function returns null (""). If you want to return the first non-blank argument, prefix the arguments with the N@ Null-if-Blank modifier.
COMCOUNT,fieldkey[,modifier=0]
Returns the count of components in the specified field/repetition from the current input HL7 message plus the optional modifier.
DATEMATH,precision,datetime,part1,value1[,part2,value2...]
Performs date math on the given value and formats the result as an HL7 date/time string using the given precision (YMDHNSZ). If you want a time only, add a "T" to the precision (e.g. "ST"). The same characters used for the precision are used for the parts. See llDates.DateMath for more information.
; Add two months and three days to a date
SET VAR=C@"DATEMATH,D,20150125,M,2,D,3" ; Returns "20150328"
; Add three and a half hours and return just the time
SET VAR=C@"DATEMATH,ST,20161229081445,H,3,N,30" ; Returns "114445"
DIGITS,string
Returns only the numeric digits from the string. Handy for stripping any punctuation from phone numbers and SSNs. The 9@ data modifier does the same thing.
EXISTS,filename
Returns "1" if the specified file exists, "0" if it does not. You may also check against wildcards (e.g. *.txt).

IF C@("EXISTS,"+V@MYFILE) == "1"
  ; Do something...
END
FIELDCOUNT,segmentkey[,modifier=0]
Returns the count of fields in the specified segment from the current input HL7 message, plus the optional modifier.
FILEPART,part,filename
Returns part of a filename. The available part values are as follows, and include example output in parentheses given a filename of "D:\Path\File.ext":
D=Directory (D:\Path)
P=Path (D:\Path\)
F=Filename (File.ext)
N=Name only (File)
E=Extension with dot (.ext)
X=eXtension without dot (ext)
V=Volume/Drive (D)
FLOATDEF,default,string[,modifier=0]
If the string is a valid numeric value, it is returned. If not, the default value is returned. The optional modifier is added to whichever value is returned. The return value will be formatted with a number of decimal places equal to the most precise of all the arguments.
FORMATDATE,formatstring,datetime
Formats a date/time value using the llDates.FormatDateTimeEx function (which is Delphi's FormatDateTime with a few of my own extensions).
SET ANSIDATE=C@("FORMATDATE,yyyy-mm-dd,"+D@"NOW") ; Returns "2017-02-21"
FPMATH,decimals,operator,num1,num2
Performs floating-point math (+-*/^) on the two numbers. The result is rounded to and formatted with the specified number of decimal places. This can also be used to round a value to a desired number of decimal places by just adding zero to it. Specifying zero decimals will truncate the result to an integer and return it without a decimal portion.
INTDEF,default,string[,modifier=0]
If the string is a valid integer, it is returned. If not, the default value is returned. The optional modifier is added to whichever value is returned.
KEYEXISTS,fieldkey
Returns "1" if the location specified by the given key exists in the input message, "0" if it does not.
LEFT,count,string
Returns the leftmost count of characters from the string. If the count is longer than the string, the entire string is returned. If count is negative, the leftmost length+count characters are returned.
SET VAR=C@"LEFT,3,foobar" ; Returns "foo"
SET VAR=C@"LEFT,-1,foobar" ; Returns "fooba"
LEN,string[,modifier=0]
Returns the length of the string plus the optional modifier.
MATH,operator,num1,num2
Performs integer math (+-*/%^) on the two numbers. Remember that integer division discards any remainder, so 5 / 2 = 2. To get the remainder you can use the modulo (%) operator: 5 % 2 = 1.
OUTKEYEXISTS,fieldkey
Returns "1" if the location specified by the given key exists in the output message, "0" if it does not.
PADC,count,string[,padchar=" "] (also PADL, PADR)
Pads the string evenly on both sides to the requested length. The PADL and PADR variations pad only the left or right side of the string, respectively. The pad character defaults to a space. If the count is smaller than the size of the string, the string is shortened. If the count argument is negative, only strings shorter than the desired length will be padded; longer strings will be unchanged.
PATHJOIN,part1,part2[,part3...]
Combines parts of a path or filename, making sure each part is properly separated with a backslash. End with a blank argument if you want the final result to end with a backslash.
SET VAR=C@"PATHJOIN,D:,Path,File.ext" ; Returns "D:\Path\File.ext"
SET VAR=C@"PATHJOIN,C:\DIR," ; Returns "C:\DIR\"
POS,substring,string[,modifier=0]
Returns the position of the substring within the string or zero if not found. The optional modifier is added to the result. The first character in a string is position one (1). POS is case-sensitive.
RANDOM,min,max[,modifier=0]
Generates a random integer in the range of min..max inclusive. The optional modifier is added to the result.
REPCOUNT,fieldkey[,modifier=0]
Returns the count of repetitions in the specified field from the current input HL7 message plus the optional modifier.
REPLACE,string,old,new (also IREPLACE)
Replaces all instances of the old value with the new in a string. The replace is done case-sensitively unless the IREPLACE variation of the function is used.
RIGHT,count,string
Returns the rightmost count of characters from the string. If the count is longer than the string, the entire string is returned. If count is negative, the rightmost length+count characters are returned.
SEGINDEX,segmentkey|fieldkey
Given a segment or field key, returns the 0-based segment index within the current input message, or blank if not present. Handy for LOOP SEGMENTS ranges.
SEGKEY,segmentindex
Given a 0-based segment index for the input message, it returns the segment key (SID#q). The segment sequence is always included, even if #1. If the index is blank or out of range, an empty string is returned.
SHELL,command
Executes an operating system command and waits for it to finish. Returns the exit code of the process or the error code returned by CreateProcess should it fail to launch.
SUBCOUNT,fieldkey[,modifier=0]
Returns the count of subcomponents in the specified field/component from the current input HL7 message plus the optional modifier.
SUBSTR,string,start[,count=MaxInt]
Returns a substring of the string starting at the specified character (the first character is at position 1). If the count is not specified, the rest of the string is returned.
TRANSLATE,table,value[,default=""]
Looks up a value in a translation table and returns its translation. If not found or the translation table does not exist, the default value is returned. Lookups are case-sensitive.
TRIM,string (also LTRIM, RTRIM)
Trims whitespace from both sides of a string. The LTRIM and RTRIM variations trim only the left or right side of the string, respectively. The P@ data modifier does the same thing as TRIM.
TZCONVERT,datetime[,fromTZ=__TZSender[,toTZ=__TZLocal[,includeOffset=1]]]
Converts a date/time value from one timezone to another, defaulting to converting from the sender's timezone to the local timezone. If the datetime value includes its own timezone, that is used instead of the fromTZ value. The format/precision of the output will match that of the input. The boolean includeOffset argument determines whether the output value includes the final timezone.
UNIQUEFILENAME,filename
Used to get a filename that is known not to exist. If the given filename does not exist, the name is returned unchanged. If it does exist, a two-digit (or more) number is appended to the end and incremented until it finds a name that does not exist and returns that.
VALIDATE,INPUT|OUTPUT[,REQUIRED][,REPEAT][,LENGTH][,DATATYPE]
If a Named Fields file is assigned (see the __NamedFields built-in variable), you can validate a message against the field definitions. The first argument determines which message is validated, INPUT or OUTPUT (or just I or O). The other optional arguments are strings indicating what to validate (only the first three characters are needed). If no options are provided, all are validated. The function returns blank if the message passes validation, or a comma-separated list of errors if it fails. If no Named Fields file is assigned, VALIDATE always returns blank. Validating DATATYPE currently checks only the first subcomponent of each repetition on simple numeric and date/time type fields (CK, CP, CQ, DT, MO, NM, SI, TM, TQ, TS).
; Validate only required fields in the input message:
SET ERR=C@"VALIDATE,INPUT,REQ"
IF V@ERR <> "" 
  LOG "Input message is missing required fields: "+V@ERR
END
VARINDEX,varname
Returns the 0-based variable index of the variable with the given name. If the variable is not defined, it returns blank.
VARNAME,varindex
Returns the name of the variable defined at the given index or blank if the index is blank or out of range.
VARVALUE,varindex
Returns the value of the variable defined at the given index or blank if the index is blank or out of range.

Return to Top

Sample Scripts

Here are some scripts I have actually used as examples of how to write your own. I will add more examples as I come up with them.

This simple script was used to analyze a batch of order messages (ORM) that failed due to invalid/undefined order frequencies. I used this to get a list and count of the frequencies and the facilities they were sent from.

;The leading underscore makes these global variables.
;Increment the count for this facility-frequency combo:
INC "_" + MSH.4.1 + "_" + OBR.27.2
;What is the grand total for each frequency?
INC "_TOTAL_" + OBR.27.2

<POST>
  LOGGLOBALS
</POST>

/* Sample output
2015-02-24 08:17:01.748 - LOGGLOBALS

_A0_IN AM=31
_A0_ONCE NOW=11
_A0_THREE TIMES DAILY PRN=4
_A0_WEEKLY=1
_D0_THREE TIMES DAILY PRN=7
_F0_THREE TIMES DAILY PRN=1
_N0_IN AM=15
_TOTAL_IN AM=46
_TOTAL_ONCE NOW=11
_TOTAL_THREE TIMES DAILY PRN=12
_TOTAL_WEEKLY=1
*/

There was an interruption of the inbound feed at a client. After restoring the feed, they sent me a file full of messages that they should have sent us during that time. I used this one-liner script to put the message control IDs into SQL statements for our inbound message store so I could make sure that we had received them all and had processed them successfully. (We did!)

OUTPUT "SELECT CONTROLID, STATE FROM INBOUND WHERE CONTROLID = " + E@MSH.10

This one I used to analyze a feed for all of the various nursing unit/room combinations coming in the patient location. Afterwards, I ended up doing a search and replace on the output to change the underscores and equal signs into commas and supplied it to the client as a csv/spreadsheet.

IF B@PV1.3.1<>"" 
  INC "_" + PV1.3.1 + "_" + PV1.3.2
ELSE
  INC "_BLANK_" + PV1.3.2
END

<POST>
  LOGGLOBALS
</POST>

This script is used to take an HL7 feed going to one system and split certain messages off to a second system. The HL7TransmitterService takes the messages from the output folders and sends them on to their destinations.

<PRE>
  ;__Debug="1"
  ; The base file path is where the script is.
  _BASEPATH=V@__ScriptPath
  PROCEDURE=SaveMsg
    ; SUBDIR must be set by the caller
    LOG "Sent to " + V@SUBDIR
    SET SAVEFILE=V@_BASEPATH + V@SUBDIR + "\" + V@__InName
    SET SAVEFILE=C@("UNIQUEFILENAME," + V@SAVEFILE)
    HL7 SAVE V@SAVEFILE
    INC SENDCOUNT
  ENDPROC
</PRE>

LOG "Processing " + V@__InName + " - " + PV1.39 + " " + PV1.18 + " " + F@MSH.9

; Copy input to output
HL7 COPYINPUT
SET SENDCOUNT="0"

; Output all ADT to 2014
IF MSH.9.1=="ADT"
  SET SUBDIR="Out2014"
  CALL SaveMsg
END

; Does this message need to go to 2015 for the IRF (ACME facility)?
; All ORU/ORM messages go to 2015 only.
; For ADT, IN is the current inpatient type and REF is a pre-admit inpatient.
IF MSH.9.1=="ORU" OR MSH.9.1=="ORM" OR (PV1.39=="ACME" AND (PV1.18=="IN" OR PV1.18=="REF"))
  SET SUBDIR="Out2015"
  CALL SaveMsg
END

; Make sure nothing has slipped through the cracks
IF V@SENDCOUNT=="0"
  ERROR "Message not forwarded: " + V@__InName
END

HL7Tools.zip includes CombineFragments.h7s, a script that will re-combine fragmented messages. That script demonstrates numerous techniques including global and local variable usage, IF and LOOP statements, saving and loading messages to files, and writing output.

Return to Top

Script Validation

In the interactive HL7Script program, there is a button labeled "Validate Script". This will syntax check the currently selected script and show any errors or warnings in the logging pane.

Each LOOP will be entered once, and all IF statements have both the main and ELSE sections run. The only code that will not get touched are PROCEDUREs that are never CALLed. No logging, output, or file-related activity (HL7 SAVE/LOAD, etc.) is actually done during validation.

Many errors are dependent upon data, so you can select a message file to use as input while validating. The first (or only) message will be read from the file and assigned to the input HL7 message. If you choose not to select a file, an ADT^A08 message that contains only an MSH segment will be used as input. It might also be a good idea to test with the mostly blank message to highlight any assumptions you may have made about the input data.

For example, a script increments a dynamic variable name based on some values in the input message:

INC "_" + PID.3 + "_" + PID.18

If PID.3 is blank, an error will be raised about attempting to increment a built-in variable because the variable name starts with double underscores. In normal usage, you probably have a condition around that line to prevent reaching it if PID.3 is blank, but validation will process that line anyway.

Keep in mind that not all validation failures actually represent errors in your script. Because every line of the script is being run once while ignoring the normal logic and branching, parts of your script will be run with unexpected data. A script that fails validation may always run perfectly in normal usage. Validation is a way to test the entire script for syntax errors and highlight possible logic errors you might not have otherwise noticed.

Return to Top

HL7ScriptService

HL7ScriptService.exe is a Windows service that will periodically poll a directory for files containing HL7 messages and process them with a script. Multiple such connections can be configured, each set to poll a different directory and use different scripts and settings.

Use the HL7ScriptServiceConfig.exe program to configure the service settings in the HL7ScriptService.ini kept in the application directory. Each connection that you configure is stored in a separate [Section] in the ini file.

The configuration program can also install and control the service when Run as Administrator. Alternately, you can use "HL7ScriptService /install" (or /uninstall) in a command prompt that was started using "Run as administrator". The service appears in the Service Control Manager as "HL7ScriptService".


HL7ScriptServiceConfig

HL7 Script Service Connection Settings
Setting NameTypeNotes
Connection Name string The name of the connection and ini [Section]. Log entries are prefixed with this name.
Disabled booleanDisable the connection without needing to delete it.
Schedule specialDetermine when processing takes place. See below.
Input File Mask string Path and wildcard for the HL7 message files to process.
Recurse Subdirectories booleanLook for files in subdirectories of the Input File Mask.
One Message Per File booleanOne message per file when checked, one or more messages per file when unchecked.
Post-Process on File/IntervalbooleanPerform post-processing after each file, or at the end of each interval.
Post-Process On Abort booleanPerform post-processing after an ABORT.
Polling Interval number Number of seconds between input directory polling.
Max Files per Interval number Limits the number of files processed per interval to keep the service responsive.
Sort Files By string Sorting helps make sure files are processed in the order received.
Script File string The script filename used to process the input files.
Named Fields File string Optional filename of an HL7NamedFields.txt file.
Archive Directory string Path to move input files to after successful processing. (blank=off)
Days to Keep Archived Files number Number of days to keep archived files. (0=forever)
Delete Files booleanIf not archiving, delete input files after successful procesing.
Error Extension string Rename files that raise exceptions so regular processing can continue. (blank=off)
Add ERR booleanAdds an ERR segment to a file renamed with the Error Extension.
Message Start Values string Comma-separated values that identify the start of a message in a multi-message file.
Connection Log string Use a connection-specific log instead of the global log.
Connection Log Level string Logging level for the connection-specific log. (N=Use global level)

HL7 Script Service Global Settings
Setting NameTypeNotes
ServiceID string See MultipleServiceInstances.txt
Log Filename string The filename may contain date substitution surrounded by percent signs (%).
Logging Level string Determines how much logging is done, ranging from None to Debug.
Show Levels booleanShows the logging level of each entry into the log with a single letter like [I] for Info.
Timestamp Format string The date/time format for each log entry. Default=yyyy-mm-dd hh:nn:ss
Days to Keep Logsnumber Number of days to keep dated log files. (0=forever)

Example HL7ScriptService.ini:

; This is a comment
LogFile=D:\Logs\HL7ScriptService%yyyymmdd%.log
LogTimeFormat=hh:nn:ss
LogDays=30
LoggingLevel=D
ShowLevels=1
NormalFormat=%s %s
LevelsFormat=%s [%s] %s
LogEOL=0D0A
WriteMS=1000
SleepMS=500
RestartPrompt=1

[HL7Split]
Disabled=0
SchedStart=0
SchedOn=1440
SchedOff=0
InputMask=D:\HL7Split\Input\*.hl7
Recurse=0
OneMessagePerFile=1
PostOnFile=0
PostOnAbort=0
IntervalMS=10000
MaxFiles=250
SortFilesBy=Time
ScriptFile=D:\HL7Split\HL7Split.h7s
NamedFieldsFile=""
ArchivePath=D:\HL7Split\Archive
ArchiveDays=15
AutoDelete=0
ErrorExt=BADHL7
AddERR=1
MessageStartValues=MSH|,FHS|,BHS|,BTS|,FTS|
ConnectionLog=""
ConnectionLogLevel=N
SleepMS=500

If the service fails to start, check the log file. If the service is unable to use the log file specified in the ini file, it will try to write to HL7ScriptService.log in the executable directory.

If you modify the ini or named fields files after the service has started, you must restart the service to load the new values. The configuration program will prompt you to do this. Changes to script files will be automatically detected at the start of each interval and will be reloaded as needed.

Any logging done in the script with LOG commands is considered to be at the default "Info" logging level by the service.

The log file can contain a date replacement format string surrounded by percent signs (e.g. "D:\Logs\HL7ScriptService%yyyymmdd%.log"). Any FormatDateTimeEx-compatible format string will do. When using a dated log file, logs older than the Log Days setting will be cleaned up automatically at startup and each time the date changes.

When using a dated log file that changes daily, the suggested Timestamp Format is "hh:nn:ss" since the date is implicit for all entries in the file.

Connection-specific logs can be used to put a connection's log entries into its own log instead of the global log. These can use the same date replacement syntax in the filename as the global log. All settings besides the filename and logging level come from the global log settings (timestamp format, days, etc.). A connection-specific log will not prefix each entry with the connection name since it will always be the same.

A connection can be scheduled. There are three options: Always on (the default), a single start and stop time, or on a cycle. A cyclical schedule means it runs for X minutes on, then X minutes off. When a schedule is set, the Schedule button text will appear bolded. If a cyclical schedule is set, it will also be italicized. The button's hint will describe the current schedule. Schedule starts and stops will appear in the log at the Verbose or Debug levels.

An output file cannot be specified when using the service. Any output done by the script must happen using FILE output or HL7 SAVE commands.

If you are not using the ArchivePath or AutoDelete settings, you will want to have the script delete/move/rename the files out of the way after processing so you don't keep processing them repeatedly. Archive cleanup (ArchiveDays > 0 or LogDays > 0) is performed at service startup and whenever the date changes.

When post-processing is done after each file, the input file is closed before post-processing so it can be deleted if needed. Single-message files (OneMessagePerFile=1) are never left open and can be deleted at any time. Global variables are reset after each post-processing.

An ABORT from the script will halt processing of all remaining files found on the current interval. It will also skip the automatic archive/deletion of the current input file (but the script can still do so). The PostOnAbort setting will determine whether post-processing is performed. The ABORT flag will be reset on the next interval.

If an exception is raised when processing a message (because of an ERROR command or something unexpected), processing will halt and both archiving and post-processing will be skipped. An entry will be made in the log noting the file name and error message. If the Error Extension setting has a value, the file will be renamed using that extension so that it doesn't keep getting picked up, preventing other files from being processed. If the Add ERR setting is turned on and this is a single-message file, an ERR segment with the error details will be added to the message when it is renamed. This allows you to see what caused the file to fail at-a-glance without having to search for the filename in the log.


Tip: If you never process billing files, you can optimize the loading of multi-message files a little bit by changing the Message Start Values setting to just "MSH|". Every line read from a file has to be checked against this list. This setting is not used when reading One Message Per File.

Return to Top

Notepad++ Language Definition

I have included HL7Script_NPPLang.xml in the distribution archive. This is my customized language export file from Notepad++. You can import this into your copy of Notepad++ by going to the Language menu, "Define your language...", and then using the Import button.

The language definition uses the "h7s" extension to identify HL7Script files. Files without this extension (like .txt files) will require the language to be selected manually. On my system, I have associated .h7s files with Notepad++ so they open automatically when double-clicked (or when the edit button is pressed in the HL7Script or config program).

If you have any ideas for improvement (other than colors), I would be happy to check them out.

Return to Top