By Joe Pluta
In the previous article in this series, I showed you how to use the INDARA to remove the *INxx indicator syntax from your programs when dealing with display files. One of the techniques was to assign a block of contiguous indicators (in my case, indicators 41-60) to act as error indicators and then redefine that block as an array of indicators. This allowed me to change a field's attributes by simply setting on the appropriate entry in the error indicator array. In today's article, I'll expand on that technique and explain how to use named indicators to streamline your programs, such as removing as much as two-thirds of your code in your screen editing routines.
Wait! Are You Saying Indicators Are Good?
I suppose I should come clean. In the most expansive use of the term, I do indeed still use indicators. However, I use the new style of indicators, named indicators, not the old-fashioned 01-99 (and KA and OF and so on). Thanks to a solid computer programming background, I am very familiar with Boolean logic, and thanks to a long history of programming in C-style languages, I am comfortable using Boolean variables. Until the advent of named indicators, though, it was rather difficult to make full use of those capabilities in RPG programs. No matter how you sliced it, using an indicator meant at some point coding the dreaded *INxx, an arcane syntax unique to the RPG language and one that frankly makes programmers from other languages raise their eyebrows (and we don't even bring up left-handed indicators in polite programming discussions).
Using numbered indicators dates back to the days of punchboards and is a technique whose time has passed. Even the slightly more civilized *INxx still has fatal flaws, whether it's the limit of 99 flags or the fact that you have to somehow know what indicator 64 is supposed to represent. Code that uses numbered indicators is very unreadable.
However, unlike the obscure *INxx, named indicators make for some very readable code. First, define the variable to have the same name as the condition it represents. For example, let's say you want to define a variable that indicates whether you have a Customer master record or not. Create it as follows:
dCustomerFound n
Defining a variable as type n specifies it as a named indicator. No length can be specified, since a named indicator must be a one-character field. In this case, if the variable is true (or *ON), it specifies that the customer was indeed found and must be updated. Otherwise, it's a new customer and the record must be added. This is a very standard design pattern in create/read/update/delete (CRUD) maintenance programs:
if CustomerFound;
UpdateCustomer();
else;
AddCustomer();
endif;
Typically, you would set this variable from the results of a CHAIN operation, like so:
custno chain CUSTMAST;
CustomerFound = %found(CUSTMAST);
Please note that I try very hard to keep the names "positive"; that is, they don't usually have words like "not" in them. In the simplest case, either would work. Let's "reverse the polarity" of the variable:
custno chain CUSTMAST;
CustomerNotFound = not %found(CUSTMAST);
if CustomerNotFound;
AddCustomer();
else;
UpdateCustomer();
endif;
This is just as good, right? However, the problem arises when you have to test for the opposite condition. In my opinion, "if not CustomerNotFound" is the kind of programming that can be very hard to understand. Polarity is a rarely discussed but very important aspect of Boolean logic, and we'll see it again shortly.
Indicators and Business Logic
Another place you might see named indicators is as a shortcut for long expressions. For example, take this credit check logic:
if (CMSTAT <> 'H'
and (OHAMOUNT + CMCURBAL) <= CMCRDLMT);
Basically, it checks that the customer is not on hold and that the purchase plus their current on-hand amount doesn't exceed their credit limit. Simple enough checks, but copying them over and over takes time and also introduces a place for inconsistencies. One option is to do the check in an edit routine and set a variable:
PassedCreditCheck = (CMSTAT <> 'H'
and (OHAMOUNT + CMCURBAL) <= CMCRDLMT);
(...)
if PassedCreditCheck;
The variable PassedCreditCheck (a named indicator) is used to store the state of the credit check algorithm. Note that you can set the value of a named indicator to any expression that returns *ON or *OFF. This powerful technique allows you to store the results of as complex a test as you want in a variable. Then, even as business rules change and regardless of how complicated the test becomes, you only have one place to change the code; everywhere else, you can just code the simple "if PassedCreditCheck." Another way to do this would be to create a subprocedure named PassedCreditCheck that does the same computation and returns the result as an indicator variable. I'll go into the pros and cons of that approach in another article.
Reducing the Code in an Edit Routine
Today, though, I want to show you how this same technique of treating a complex expression as a simple Boolean variable can reduce your editing logic significantly. Let's take a standard maintenance panel editing screen:
if XXCUST = 0;
Errors(F_XXCUST) = *on;
SendError(E_ZEROCUST);
endif;
Even if your code doesn't look exactly like this, you should be able to recognize what it does. If the customer number is 0, it sets on an error indicator. (This uses the INDARA technique from my previous article to create an array of Boolean indicators, each one representing a specific field.) I advanced the technique a little; for readability, I defined constants, one for the index of each field. So F_XXCUST represents the position in the array of the indicator for the XXCUST field. Next, I send an error message. In my programs, I use a message subfile. The SendError subprocedure accepts a message ID and does all the rest of the grunt work to send the message. I use a constant to identify the message ID.
So, including the IF and ENDIF, every test requires at least four lines of code. And even if I combined setting the error indicator with sending the message functions into a single SetError procedure, I'd still end up with code like this:
if XXCUST = 0;
SetError(F_XXCUST : E_ZEROCUST);
endif;
if XXNAME = *BLANKS;
SetError(F_XXNAME : E_NAMEBLANK);
endif;
if XXCITY = *BLANKS;
SetError(F_XXCITY : E_CITYBLANK);
endif;
If a master record has 20 tests, at three lines per test, the comparisons alone could expand to well over 50 lines of code, not counting the additional business logic to chain out to other files to verify key values and so on. However, I have a way to reduce that code to just one line per test:
TestError((XXCUST = 0) : E_ZEROCUST: F_XXNAME);
TestError((XXNAME = *BLANKS) : E_NAMEBLANK : F_XXNAME);
TestError((XXCITY = *BLANKS) : E_CITYBLANK : F_XXCITY);
What's this? Well, in this technique you can write a procedure that takes three parameters: the field whose error to set, the message ID to send, and the condition to test. I think the last two parameters are pretty straightforward, but what is the first one? Well, remember that anywhere you have a named indicator, you can substitute a Boolean expression: one that evaluates to *ON or *OFF. For example, (XXCUST = 0) will either return *ON if customer is 0 or *OFF if it is not. The cool part is that if you define a parameter to a procedure as a named indicator, then you can specify an expression on the call. The system will evaluate the expression at runtime and pass the result, *ON or *OFF, to the procedure (note that to allow this automatic evaluation, the parameter must also be a constant parameter identified by the keyword CONST as shown).
The code for the TestError procedure is very straightforward:
pTestError b
d pi
d Condition n const
d MsgID 7a const
d FieldIndex 3u 0 const
/free
if condition;
SendError(MsgID);
Errors(FieldIndex) = *on;
endif;
/end-free
p e
It takes three parameters: the condition, the message ID, and the field index. If the condition is true, the message is sent and the field's indicator is set on. This little routine can make your programming much more compact and easy to read, and with a liberal application of constants to identify the field indices and error message IDs, you can also insulate yourself from changes quite nicely.
Please note that the routine above is pretty simple. It assumes a one-to-one relationship between errors, messages, and fields: it always sends a single message and sets a single field for each error. I usually use a slightly more robust technique in production programs:
pTestError b
d pi n
d Condition n const
d MsgID 7a const
d FieldIndex1 3u 0 const options(*nopass)
d FieldIndex2 3u 0 const options(*nopass)
d FieldIndex3 3u 0 const options(*nopass)
/free
if condition;
if MsgID <> *blanks;
SendError(MsgID);
endif;
if (%parms > 2);
Errors(FieldIndex1) = *on;
endif;
if (%parms > 3);
Errors(FieldIndex2) = *on;
endif;
if (%parms > 4);
Errors(FieldIndex3) = *on;
endif;
endif;
return condition;
/end-free
p e
These changes allow a little more flexibility in how the routine is called and also allow some serious chaining of conditions as needed. For example:
if TestError(cond1 : E_MSG1 : F_FLD1)
or TestError(cond2 : E_MSG1 : F_FLD2A : F_FLD2B)
or TestError(cond3 : E_MSG3);
(... errors ...)
else;
(... no errors ...)
endif;
I can test multiple conditions, sending the error messages and setting fields in error. Note that condition two sets the error flag on two different fields (for example, two UOM fields that have no cross-reference or two fields in a multi-field key), while condition three only sends a message, setting no fields (a record lock or some other unexpected error occurs). But because the TestError procedure returns the condition, I can test it, and if any of the error conditions exist, I will execute the error-handling logic; otherwise, I handle the success logic.
Final Note
In reality, the code is often not as simple as this; you might find yourself executing chains to other files to get information or calling other programs or procedures. Some errors are so severe as to stop processing altogether. Other messages need to be sent back to the calling program (another great topic for another day). But no matter what, the idea of using expressions as variables--whether they are passed as parameters or used as Boolean variables later in your code--can simplify your RPG code and make it more readable.
LATEST COMMENTS
MC Press Online