HL7Script is used to perform a sequence of instructions based on the contents of one or more HL7 messages from an input file or wildcard. These instructions allow you to analyze or transform messages and generate 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. Items shown in square brackets are optional.

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.

$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 parent script file is assumed. The filename must be a literal value because included scripts are loaded prior to initialization -- you cannot use any variables or script logic to create the filename.

Included scripts may not contain any of the optional sections such as pre-processing. 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

Script Sections

A script is divided into sections. Only the "Main" script, the part that applies to each message processed, is required. All of the other sections are optional and are surrounded by tags to separate them from the rest of the script.

<INIT>
    ; Initialization section
</INIT>
<PRE>
    ; Pre-processing section
</PRE>

; Anything not in one of the other sections is the Main script. 
; The Main script is run for each message processed and must
; contain at least one line of code.

<POST>
    ; Post-processing section
</POST>
<FINAL>
    ; Finalization section
</FINAL>

Initialization

The initialization section is processed only once when the script is first loaded. Its syntax is limited to initializing variables, setting up translation tables, and creating 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 <INIT> and </INIT> on lines by themselves.

; Example Initialization Section
<INIT>
  TRANSLATION=tablename
    input=output
    FOO=BAR
  ENDTRANS
  PROCEDURE=procname
    INC _CALLCOUNT
    LOG "You have called this procedure " + V@_CALLCOUNT + " times."
  ENDPROC
  LOADVARS "D:\HL7\vars.txt"
  %PersistVar="value"
  __Debug="1"
</INIT>

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. You may have as many translation tables defined as you need as long as each table name is unique. The table name, input, and output values need not be quoted unless they contain important whitespace or reserved words. Translation table lookups are case-sensitive unless the ITRANSLATION keyword is used. Each translation table must be finished with ENDTRANS on a line by itself.

User-defined procedures can also be defined in the initialization 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 other parts of the 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.

You may also make calls to LOADVARS in the initialization section:

LOADVARS "D:\HL7\vars.txt"

Any other lines in the initialization section are considered to be variable assignments with a "name=value" format:

%PersistVar="value"
__Debug="1"

Typically, only persistent or built-in variables would be set in the initialization section.

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

Pre-Processing

The pre-processing section is run at the start of each processing interval. In the interactive HL7Script program, this would be whenever the "Process" button is pressed. In the service application, this would be at the beginning of each interval or file, depending on options. Here you could do things like initialize variables that are interval-specific.

If present, it must follow the initialization section or be located at the start of the script. The section must be surrounded by <PRE> and </PRE> on lines by themselves.

There are no syntax restrictions in the pre-processing section like there are for the initialization section. All regular script commands are available, but the input HL7 message is undefined and must not be referenced.

Post-Processing

The post-processing section 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.

If present, it must be located after the main script and be surrounded by <POST> and </POST> on lines by themselves.

<POST>
  LOGVARS
</POST>

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

Like pre-processing, all regular script syntax is available in the post-processing section and the input message is undefined.

Finalization

The finalization section is processed just once, either at service shutdown or at the end of processing in the interactive HL7Script program. The finalization section is always run, regardless of any abort or error conditions.

If present, it must be the last section of the script and be surrounded by <FINAL> and </FINAL> on lines by themselves. There are no syntax restrictions.

<FINAL>
  SAVEVARS "D:\HL7\vars.txt" "%*"
</FINAL>

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 any valid expression; a string literal, an HL7 data value, another variable, or a concatenation of multiple values.

SET FOO="bar"
SET NEWID = PID.8 + "." + V@FOO
SET STARTTIME = Z@"NOW"

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

SET NEWID=""

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

SET BAR = V@FOO
OUTPUT V@NEWID

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!

Scope and Lifetime

All variables are global in scope. A variable set anywhere in the script can be referenced anywhere else in the script. However, how long a variable lasts, its lifetime, is based on how it is named. Some variables live only as long as the section they're created in is running, some last for the span of a few sections, and some are effectively permanent.

Section variables have no special prefix. Once set, they exist until processing of the current section is completed or the variable is cleared. Often called "automatic" variables. These are useful for short-term usage like loop counters or temporary string manipulation.

Interval variables are prefixed with a leading underscore (_) and are reset each time pre-processing takes place or when deliberately cleared. These allow you to collect information or totals from all of the messages in the current interval.

Persistent variables are indicated with a leading percent sign (%) and last forever (as long as the service is running) or until cleared. Persistent variables are useful for maintaining long-term state or uptime/lifetime counts.

Service variables are indicated with leading double percent signs (%%) and are shared among all of the connections (threads) of the service. A variable set by one connection can be read by another. Otherwise, they behave like persistent variables. They are automatically loaded at service startup and saved at service shutdown in a file called "HL7ScriptServiceVars.txt" in the application directory. If you do not make use of service variables, the file will not be created.


Variable Lifetimes
SectionVariables Cleared at Start
Initialization Section, Interval, Persistent
Pre-Processing Section, Interval
Main Script Section
Post-ProcessingSection
Finalization Section, Interval

In the interactive HL7Script.exe program, persistent and service variables are effectively no different than interval variables due to the short lifespan of the script. Service variables are neither loaded nor saved when using HL7Script.exe.

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" interval variable would have a value of "7" at the end of the input.

To reference a dynamically-named variable, either use parentheses with the V@ modifier to surround the name expression, or set another variable to the desired name and use double-dereferencing (V@V@):

; Modifier Parentheses
IF V@("_" + PID.3.1) == "1"
  LOG "First appearance of MRN " + PID.3.1
END

; Double-V@
SET MRNVAR="_" + PID.3.1
IF V@V@MRNVAR == "1"
  LOG "First appearance of MRN " + PID.3.1
END

SAVEVARS and LOADVARS

Section variables last only until the end of the current section, interval variables get reset at the start of pre-processing, and persistent variables last only as long as the service is running. If you need some values to last longer, 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 used in the finalization section to store your persistent variables. 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 user variables. A percent sign and asterisk "%*" will save all persistent variables. An underscore and asterisk "_*" will save all interval 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\Persistent.txt" "%*"

LOADVARS can also be called anywhere, but would most likely be used in initialization. It expects just a filename, and will load any variables stored in the file. Just put a space between LOADVARS and the filename.

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.

;Initialization example
<INIT>
  LOADVARS "C:\Data\Vars.txt"
</INIT>

;Main script example
IF V@%VARSLOADED<>"1"
  ; Only do this once
  LOADVARS "C:\Data\Vars.txt"
  SET %VARSLOADED="1"
END

Do not try to share a variable file among multiple connections. SAVEVARS and LOADVARS are not threadsafe. Service variables serve this purpose.

Built-In Variables

There are a number of "built-in" variables available to the script. These built-in variables all start with double underscores (__). 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 or message 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.
__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 user variables (section, interval, and persistent).

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 intervals; it will remain assigned until changed to something else or the service is stopped. Setting it to blank will unassign an anonymizer definition. To reset the anonymizer's persisted data (if not using a DataStore file), re-assign the variable. It can be done in Initialization or within the main script.
; Set the anonymizer up just once in the main script:
IF V@__Anonymizer==""
  SET __Anonymizer="D:\HL7\Generic.anon.ini"
END
An anonymizer definition that saves increments or uses a data store should not be used by multiple connections at once; the anonymizer is not threadsafe.
__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.
__CSVNewLine
When processing a LOOP CSV this value is used to represent a line break in the event a multi-line quoted field is encountered. Default = "\.br\". (See llCSV.ParseCSVLine for details)
__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
Boolean, default is False. Logs every expression evaluated as data. This generates LOTS of logging!
__Epsilon
Numeric, default=0. 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.
__FileEncoding
Sets the encoding used for FILE output. Setting the value to blank or "DEFAULT" selects Windows' default single-byte ANSI codepage. Setting it to "COPYINPUT" copies the encoding currently being used by the input HL7 message. Any other value (e.g. "UTF-8", "1251", etc.) is used as-is. Specifying an invalid encoding name will raise an exception.
__FileEOL
The end-of-line character(s) used for FILE output. The default is CRLF. 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.
__LowHexOnly
Gets/sets the boolean LowHexOnly message property on the Input and Output HL7 messages. By default (False), 8-bit characters are escaped only when the MSH.18 character set value is blank or "ASCII". When True, 8-bit characters will be ignored in hex escaping regardless of the MSH.18 character set value. Note that when __MinimalHex is False and low binary characters (<0x20) are present, the entire subcomponent is still hex escaped.
__MaxLoops
Numeric, default=1000. The maximum number of loop iterations allowed. This prevents an endless loop from hanging the program by throwing an exception when it exceeds this number.
__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, limited to just the characters that require it. When False, the entire subcomponent is hex escaped if any part of it requires encoding. The default is True or is 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 intervals; it will remain assigned until changed to something else or the service is stopped. 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 a valid numeric value (blank, "foo", HL7 null, etc.). When True (the default) an exception is raised, halting execution of the script. If set to False, 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.

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"
I@ - Reserved
J@ - Reserved
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.
Q@ - Reserved for Query
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)
W@ - Reserved
X@ - Reserved for EXEC
Y@ - Reserved
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) 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, the unary NOT operator has precedence and is applied to the expression on its right first. Then, logical operators are evaluated from left to right. The expression "1 OR NOT 0 AND 1" evaluates as if it were written as "(1 OR (NOT 0)) AND 1". Tip: Use parentheses to avoid ambiguity.

IF statements require some kind of comparison to take place to generate a boolean result. You may specify any of a number of operators (all of which are two characters in length for ease of parsing) when comparing values:


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 repeat a series of commands as long as the loop condition remains true. That condition varies based on what kind of loop is desired.

Single question marks (?) within the loop block are replaced by the 1-based numeric index on each loop. Double question marks (??) are replaced with the 0-based index. For example, the first time through the loop, "?" would be replaced by "1", and "??" would become "0". On the next loop, "?" would be "2" and "??" would be "1", etc.

Most loop types may be nested. In nested loops, only the original condition from the outer loop (e.g. IN1#? or PID#1.3~?) is replaced inside the nested loops. Other inner loop question marks receive their value from their own loop counter.

; Nested loop example
LOOP OBX
  LOG "?" ; This gets its value from the outer loop
  LOOP OBX#?.5 ; OBX#? gets replaced by the outer loop
    LOG " ? " + OBX#?.5~?.1 ; OBX#? from outer, other ?s from inner
  END
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 a loop 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.

Expression Loops

An expression loop will repeat so long as the expression following LOOP evaluates to True. The expression is the same as one that would be used in an IF statement and is identified by the presence of one or more of the comparison operators.

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

Segment ID Loops

To iterate over a certain type of segment, use a 3-character segment ID after LOOP. For example, LOOP DG1. The loop will be run once for each segment matching the given segment ID. If no such segments exist, the loop will not be entered.

Inside the loop use the segment ID with a question mark segment sequence to represent the current segment, e.g. 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 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 DG1#?" ; This is true!
  END
END

Segment Index Loops

To loop through the segments of the input message based on position rather than segment ID, use LOOP SEGMENTS.

By default, all segments of the message are included. If you want only a subset of the segments, you may specify a numeric range in LOOP SEGMENTS [start[-end]] format.

The first segment is segment zero (usually MSH). If no end segment index is given, 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. It will be replaced with the segment ID and sequence, e.g. "MSH#1" or "IN1#3". 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

SEGMENTS loops may not be nested within other SEGMENTS loops.

Field Repetition Loops

To iterate over a repeating field, use a field key as the loop condition: LOOP PID#1.3. The loop is run once for each repetition of the given field, or skipped if the field is not present. A field that is present but blank has one (blank) repetition.

Within the loop, use a question mark after the repetition separator to represent the current repetition, e.g. 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 field key.

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

Text and CSV Loops

Use LOOP TEXT or LOOP CSV to read a text file and loop over each line of the file. Follow TEXT or CSV with a space and the name of the file to be read. This could be especially handy when using non-HL7 files as input.

The section variable LOOPTEXT is set to the value of the current line at the start of each loop. When using LOOP CSV, the line is also parsed as csv data. The section variable CSVCOUNT contains the number of fields parsed from the current line, and you can call the CSVFIELD script function to retrieve the values of those fields. If the line is blank, CSVCOUNT will be zero. If the line fails to parse, CSVCOUNT will be set to "-1".

; Example with a non-HL7 text or csv file as input
LOOP CSV V@__InFile
  LOG "Line ?: " + V@LOOPTEXT
  ; The IF allows testing LOOP TEXT or LOOP CSV
  IF V@CSVCOUNT <> ""
    LOG "Field count: " + V@CSVCOUNT
    SET X="0"
    LOOP V@X #< V@CSVCOUNT
      SET FIELDVALUE=C@("CSVFIELD," + V@X)
      LOG "Field " + V@X + ": " + V@FIELDVALUE
      INC X
    END
    LOG ""
  END
END

Since the number of lines in a text file may be considerably higher than the normal setting for __MaxLoops, that limit is not enforced when using one of these loops.

TEXT/CSV loops may not be nested inside other TEXT/CSV loops. LOOPTEXT and CSVCOUNT are cleared when the loop ends.

QUIT, ABORT, and ERROR

QUIT can be specified 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, wildcard, or interval. 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 MSH.11 == "P"
  ERROR "Production messages in the Test system!"
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 initialization 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:

<INIT>
  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
</INIT> 

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

Return to Top

LOG Commands

The LOG command is used to write information to the log file (or screen when using the HL7Script program). The expression following LOG is evaluated and written to the log 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 [wildcard [NOPREFIX]]
Sorts the variables by name and then writes them all to the log in "Name=Value" format, one per line. If a wildcard is specified (see SAVEVARS for wildcard syntax), only the variables with names matching the wildcard are logged. Specifying NOPREFIX will strip the wildcard from the start of the variable names before logging them.
LOGSCRIPT
Dumps the entire script to the log with line numbers. Blank lines, comments, and whitespace will have been removed from the script during initialization.
LOGBLOCK
Like LOGSCRIPT, but only shows the lines from the IF/LOOP block currently being processed. The IF/ELSE/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

Output and File Commands

OUTPUT works much like the LOG command. The expression following OUTPUT is evaluated and written to the output file as a line of text. If no ouptut file is available, output goes to the log instead.

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

The end-of-line characters used by OUTPUT are controlled by the __FileEOL built-in variable, and default to a carrige return and line feed (CRLF, 0x0D0A). If you are writing an HL7 output file, you would want to set it to a carriage return only.

The OUTPUTX command is the same as OUTPUT, but does not include a line terminator in case you need finer control over how your output file is constructed.

The file's encoding is controlled by the __FileEncoding variable, defaulting to the system's default ANSI codepage. Other values like "UTF-8" or "1251" can also be selected.

; Change file output to use HL7 line terminators and UTF-8 encoding
SET __FileEOL=H@"0D"
SET __FileEncoding="UTF-8"

To prepare or manipulate the file used by OUTPUT, use one of the FILE commands:

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 an existing target file.

If no explicit path is provided in the 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.

Only one output file may be open at a time. The file remains open until closed or the end of the current section.

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

Return to Top

Base64 Commands

The Base64 commands are used to load and save binary files, encoding or decoding their content in Base64 format. This can be especially handy if you are using non-HL7 input such as pdf files.

BASE64LOAD filename varname
Reads the specified file from disk, encodes the contents as Base64, and stores that encoded data in a variable.
; Load the input pdf file and store the data in an OBX segment:
BASE64LOAD V@__InFile B64DATA
HL7 SET OBX.5=V@B64DATA
SET B64DATA="" ; Could be big - tidy up!
BASE64SAVE filename base64data
Decodes Base64 data and writes it to the specified file.
; Retrieve pdf data from an OBX segment and save it to disk:
SET PDFFILE=V@__InPath + OBR.2 + ".pdf" ; Use the order number as the filename
BASE64SAVE V@PDFFILE OBX.5

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 ENCODING DEFAULT|COPYINPUT|encoding
Sets the encoding on the output message for use by APPEND and SAVE. DEFAULT selects Windows' default single-byte ANSI codepage. COPYINPUT copies the encoding currently being used by the input HL7 message. Any other value (e.g. UTF-8, 1251, etc.) is used as-is. Specifying an invalid encoding name will raise an exception.
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
Writes the output HL7 message to the active output file, one segment per line. The line terminator of the output file is controlled by the __FileEOL variable.
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

XML Commands

A common non-HL7 input format is XML. These commands allow you to read and write XML documents.

The commands use standard XPath notation to identify elements and attributes. You can use absolute paths that start at the document root, or relative paths based on the currently selected node. Square brackets surround the index of repeating elements. An at-sign (@) indicates an attribute. XML is case-sensitive.

After each XML command, you can check the value of the boolean section variable XMLOK. If set to "1", the command was successful. If "0", the last XML command has failed. The most common failure would be attempting to select or read an element or attribute that does not exist.

The following commands are used when reading an XML file:

XML OPEN filename
Opens the specified file. Only one XML document may be open at a time. A document remains open until closed, another document is opened, or the script section ends.
XML CLOSE
Closes any open XML document and clears the XMLOK variable.
XML SELECT xpath
Selects the specified node as the "active" node. Subsequent XML commands can use relative XPath values to access data. If the requested node does not exist, the active node will not change and XMLOK will be set to "0".
XML GET variable=xpath
Reads the value of an element or attribute and stores it in the specified variable. If the value does not exist, the variable will be cleared and XMLOK will be set to "0".

The commands for writing XML are limited, but functional. The commands listed above are also available when working with a new XML document.

XML NEW rootnode
Create a new UTF-8 encoded XML document with a root node of the given name. The root node is selected as the active node.
XML SAVE filename
Saves the document to the given filename. The document remains open.
XML ADDCHILD nodename=text
Adds a child node with the given name and text to the active node. The text may be blank, but must be present. The new child node is selected as the active node.
XML DELATTR attributename
Deletes the active node's attribute of the given name. XMLOK will be set to "0" if the attribute is not found.
XML DELCHILD nodename
Deletes the matching child of the active node. The node name may include an index, such as "child[2]", to delete the Nth node matching the name. XMLOK will be set to "0" if a match is not found.
XML SETATTR attributename=value
Creates or updates the attribute of the given name on the active node.
XML SETTEXT value
Sets the text of the active node to the new value.

Using commands other then OPEN, CLOSE, or NEW without an active XML document will raise an exception.

The following example uses an rss.xml file as input, since that is a commonly understood XML standard and allows the concepts to be easily demonstrated.

XML OPEN V@__InFile
IF V@XMLOK == "0" 
  ERROR "Failed to open XML file: " + V@__InFile
END
SET ITEM="1"
; This selects a node with an absolute (from the root) path:
XML SELECT "/rss/channel/item[" + V@ITEM + "]"
; Loop until the requested node doesn't exist:
LOOP V@XMLOK == "1"
  ; Get data using relative paths (. = active node):
  XML GET TITLE="./title"
  XML GET GUID="./guid"
  XML GET IPL="./guid/@isPermaLink" ; An attribute of the guid
  LOG "Item ?: title=" + V@TITLE + ", guid=" + V@GUID + ", isPermaLink=" + V@IPL
  INC ITEM
  ; SELECT can also use relative paths (.. = active node's parent)
  XML SELECT "../item[" + V@ITEM + "]"
END
XML CLOSE

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 that is added to the result. This is a convenience to save you a separate call to MATH or INC/DEC. 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 case-insensitive alphanumeric (A-Z and 0-9). 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.
CSVFIELD,index
When inside a CSV loop, it returns the value of the csv field at the given index. The first field is index zero. The CSVCOUNT section variable holds the count of fields currently available. Calling CSVFIELD with an out-of-range index will simply return a blank. Attempting to call CSVFIELD outside of a CSV loop will raise an exception.
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"
FORMATDIGITS,formatstring,digits[,escapechar]
Right-justifies/overlays a string of characters (usually digits) into a format string. Especially handy for phone number/SSN formatting, but it could conceivably be used on any type of input. See llStrings.FormatDigits for more information.
SET TEST=C@("FORMATDIGITS,(099)999-9999,6025551212") ; Returns "(602)555-1212"
SET TEST=C@("FORMATDIGITS,(099)999-9999,5551212")    ; Returns "555-1212"
SET TEST=C@("FORMATDIGITS,999.999.9999,6025551212")  ; Returns 602.555.1212
SET TEST=C@("FORMATDIGITS,foo,bar")                  ; Returns "foobar"
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. A translation table's definition determines its case sensitivity.
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. Note that only numeric and date/time data types can truly be validated without context. Everything else is essentially just a string.
; 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 name is blank or 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.

;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>
  LOGVARS
</POST>

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

_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>
  LOGVARS _* NOPREFIX
</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.

<INIT>
  ;__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
</INIT>

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 interval and section 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 other file-related activity (HL7 SAVE/LOAD, etc.) is actually done during validation. Any non-HL7 input like XML or LOOP TEXT/CSV has simulated input when validating.

Many errors are dependent upon data, so you may select what to use for input while validating. You can choose an HL7 file (the first message will be used as input), a default HL7 message in the form on an ADT^A08 message that contains only an MSH segment, or non-HL7 input which supplies a blank input message. With HL7 input, it may be helpful to test with the mostly blank message to highlight any assumptions that may have been made about the data.

For example, a script increments a dynamically named variable 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 and process them with a script. The files may contain HL7 messages or other kinds of data. Multiple 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. The settings are stored in HL7ScriptService.ini 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 (which it does by default). 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".

When upgrading, it is always a good idea to run the config program before restarting the service. It will alert the user if there are important changes and allow the settings to be reviewed and saved. If the version stored in the ini differs enough from the current service version, the service will refuse to start until the ini file is updated.

When the service is running, the "Start" button changes to "Connection Status". Press the button to open a dialog showing the current status, last activity time, and uptime message count for each active connection.


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. Appears as strikethrough.
Schedule specialDetermine when processing takes place. See below.
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)
Input File Mask string Path and wildcard for the input files to process.
One Message Per File booleanOne message per file when checked, one or more messages per file when unchecked.
Non-HL7 booleanThe input is not HL7 data. The input message supplied to the script will be blank.
Recurse Subdirectories booleanLook for files in subdirectories of the Input File Mask.
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.
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.
Named Fields File string Optional filename of a Named Fields file for field keys and message validation.
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.

HL7 Script Service Global Settings
Setting NameTypeNotes
Service ID string See Multiple Service Instances.
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)

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 to see when the next message starts. This setting is not used when reading One Message Per File.

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. If the script needs to be reloaded, the current finalization section will be run prior to doing so.

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

The log filename 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.

The log date replacement is assumed to change no more frequenly than daily, but may be longer (weekly, etc.). When using a daily log file, the suggested Timestamp Format is "hh:nn:ss" since the date is implicit for all entries in the same 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.

When a connection is using non-HL7 input files, the input message supplied to the script is empty. The script must know how to handle the input file based only on its name. The filename is available in the __InFile built-in variable. Options like LOOP TEXT, LOOP CSV, BASE64LOAD and BASE64SAVE, and XML commands are available for processing alternate input.

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.

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 (either unexpected or because of an ERROR command), 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 error extension contains an asterisk, it will include the file's original extension rather than just replacing it. If the Add ERR setting is turned on and this is a single-message HL7 file, an ERR segment with the error details will be added to the message when it is renamed. This allows you to see at-a-glance what caused the file to fail without having to search for the filename in the log.

Multiple Service Instances

HL7ScriptService and HL7TransmitterService can be configured to run multiple instances on a single server. A common use for this would be running both a Test and Production instance on the same server, possibly of different versions. The instructions are the same for either service.

Set up two (or more) separate application directories, each with its own copies of the executables. Run the configuration program in each directory to create the ini file. After configuring the regular settings, press the Service ID button. Enter a value that will uniquely identify the service running from each directory. Keep the value short and alphanumeric, like "Test" or "Prod".

The Service ID button will make sure your chosen ID is not already in use, and will automatically uninstall/reinstall the service as needed to change an existing ID. Any changes you may have made to the service startup type or logon account will be preserved for you. The Service ID is saved to the ini file.

When a ServiceID has been assigned, the configuration program will show the ID in square brackets in the Service Control area's status text:

HL7 Service ID

The service will appear in the Services management console with the ServiceID value appended to the base service name. If you had set up "Test" and "Prod" instances of HL7ScriptService, you would see the entries named HL7ScriptServiceTest and HL7ScriptServiceProd.

Each service can then be configured, started, stopped, or uninstalled independently of any others.

Each instance must have its own unique log filename(s); they must not try to share a common log file or they will fight over who gets to write to it.

Return to Top

Notepad++ Language Definition

I have included HL7Script_NPPLang.xml in the distribution archive. This is my customized language export file for 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).

Return to Top