You are on page 1of 30

RPG Tips and Techniques

Jon Paris

Jon.Paris @ Partner400.com
www.Partner400.com
www.SystemiDeveloper.com

Notes
About Me:
I am the co-founder of Partner400, a firm specializing in customized education and mentoring services for IBM i (AS/
400, System i, iSeries, etc.) developers. My career in IT spans 45+ years including a 12 year period with IBM's
Toronto Laboratory.

Together with my partner Susan Gantner, I devote my time to educating developers on techniques and technologies
to extend and modernize their applications and development environments. Together Susan and I author regular
technical articles for the IBM publication, IBM Systems Magazine, IBM i edition, and the companion electronic
newsletter, IBM i EXTRA. You may view articles in current and past issues and/or subscribe to the free newsletter at:
www.IBMSystemsMag.com. We also write frequently for IT Jungle's RPG Guru column (www.itjungle.com).

We also write a (mostly) monthly blog on Things "i" - and indeed anything else that takes our fancy. You can find the
blog here: ibmsystemsmag.blogs.com/idevelop/
Feel free to contact me any time: Jon.Paris @ partner400.com

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 1 of 30


Agenda
Basic Thoughts

Ctl-Opt (H Spec)

Compiler Directives

Integers

Varying Length Fields

Indicators - they need names !

Templates - Design your own data types

Messaging - Versatility is key - lose your green screen habits

New Approaches to I/O operations

Using modern error handling options

Short form expressions

Use offsets and pointers when appropriate

Basic Thoughts
Be Free
• Code EVERYTHING in free form
• Consider changing old programs over to free when making
significant modifications
✦ Particularly if it is already in RPG IV
• There are excellent (and cheap) conversion tools out there
✦ And very very few compatibility issues
Use proper names
• If you use a good editor like RDi there's no increase in typing
• Use customerName not custNam not cusNo not wkCsNo
✦ messageType not msgTyp
Speak the same language as the rest of the world
• Table not Physical File
• Index or View not Logical File

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 2 of 30


Basic Thoughts
Learn and adopt from other languages
• Use /INCLUDE instead of /COPY
• Constant names in all CAPITALS
• Procedure names begin with upper case, variables with lower
Use a modern tool set
• Preferably RDi
✦ Or miWorkplace or ILEditor if budget is an issue
Embrace what is new
• Learn new habits
• Learn new languages
✦ They will improve your RPG !

H-spec / Ctl-Opt
These have a new lease on life in RPG IV
• They can supply defaults for dates/times in the program
• Control debugging options
• Compiler options - to ensure the same ones are always used
The compiler stops looking once the first of these is found
• A Ctl-Opt (or H-spec) included in your source
• A data area named RPGLEHSPEC in *LIBL
• A data area named DFTLEHSPEC in QRPGLE
Tip: DO NOT use the data areas!
• If you want standard set of options then use a /Copy or /Include

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 3 of 30


Compiler Options
Many CRTBNDRPG/CRTRPGMOD keywords can be specified
• Options supplied override any specified on the CRTxxxxxx command
Unsupported keywords:
• DBGVIEW, OUTPUT, REPLACE, DEFINE, PGM, SRCFILE, SRCMBR,
TGTRLS
Use Options( *SrcStmt : *NoDebugIO )
• *SrcStmt matches program statement numbers with source line numbers
✦ Easier for debugging and end user support
• *NoDebugIO skips the individual field moves on I/O operations
✦ Faster step function during debugging
✦ Really useful when large numbers of fields are involved

ctl-Opt option(*SrcStmt :*NoDebugIO);


ctl-Opt datFmt(*ISO) timFmt(*ISO) datEdit(*YMD-);
ctl-Opt decPrec(63);
ctl-Opt copyRight('This can be useful for version numbers’);

Notes
Multiple options are separated with a colon (:) – e.g OPTIONS( *SRCSTMT : *NODEBUGIO)
With *SRCSTMT specified, the statement number reported when an error occurs during run time will correspond
directly to the SEU sequence number. Without this support, the statement number reported did not correlate directly
to the source statement numbers. Therefore, support of end user problems was much more difficult. Many support
desks kept compiler listings of all programs just to be able to match the program statement numbers to SEU
statement numbers.

*NOSRCSTMT (default behaviour) indicates that the compiler just "makes them up" and assigns line numbers
sequentially.

If *SRCSTMT is specified, statement numbers for the listing are generated from the source ID and SEU sequence
numbers as follows: stmt_num = source_ID * 1000000 + source_SEU_sequence_number

If *DEBUGIO is specified, breakpoints are generated for all input and output specifications. *NODEBUGIO (the
default) indicates that no breakpoints are to be generated for these specifications. This means that during debug
sessions, doing a Step function on an I/O operation required many steps (one for each field in the format).

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 4 of 30


Compiler Directives
Compiler directives test and act on Condition Names
• They are used to control which source statements are included
• Enables the use of a single source for different versions of an application
Within the source conditions can be set and cleared
• by /DEFINE ( condition-name ) and /UNDEFINE ( condition-name )
They may also be set on the compiler commands
• CRTBNDRPG and CRTRPGMOD
Directives available are:
• /IF {NOT} DEFINED ( condition-name )
✦ Include the following source lines if the test is true
• /ELSEIF {NOT} DEFINED ( condition-name )
✦ You can guess this one ...
• /ENDIF
✦ End of the current conditional block
• /EOF
✦ Jump straight to the end of the current source file

Compiler Directives - Predefined Condition Names


Compiler has a number of predefined condition names
• *CRTBNDRPG – True if compiling with CRTBNDRPG
• *CRTRPGMOD – True if compiling with CRTRPGMOD
• *VxRxMx – True if compiling for the specified release or later

/if defined(*CRTBNDRPG)
ctl-Opt dftActGrp(*NO) actGrp(‘MYACTGRP');
/endIf

ctl-Opt bndDir(‘MYBNDDIR');
ctl-Opt option(*srcStmt :*noDebugIO);
ctl-Opt datFmt(*ISO) timFmt(*ISO) datEdit(*YMD-);
ctl-Opt decPrec(63);

/if defined V7R3M0


// Use this sexy one liner new opcode
/else
// Use the old way of doing it
/endIf

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 5 of 30


Use Integers
True Binary representation
• Data types of INT (signed Integer) or UNS Unsigned Integer
Replaces the old BINDEC (Binary) data type
• Has a broader range for the same size
• No conversion to/from packed (as with binary)
• Use instead of B data type with APIs
Use for compatibility with other languages
• C, Java and MI (and CL in V5R3)
• Used extensively with C functions and APIs
Better performance for math operations
• Faster then packed (or zoned or binary)
Use integers for all loop counters etc.
• And for any numeric that does not require decimals
Never use type B (BinDec)
• Even if IBM's docs do show it in examples

Integer Details
Integers are identified in different ways in different places
• It depends on the intended audience for the documentation
• RPG, C and Java use different notations to those for APIs

RPG Bytes Range of Values API C and Java


INT(3) 1 -128 to 127 BIN1 char
INT(5) 2 -32,768 to 32,767 BIN2 short
INT(10) 4 -2,147,483,648 to 2,147,483,647 BIN4 long or int
INT(20) 8 -9,223,372,036,854,775,808 to BIN8 long
9,223,372,036,854,775,807
UNS(3) 1 0 to 255 UBIN1 Unsigned char
UNS(5) 2 0 to 65535 UBIN2 short
UNS(10) 4 0 to 4,294,967,295 UBIN4 long
UNS(20) 8 0 to 18,446,744,073,709,551,615 UBIN8 long long

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 6 of 30


Varying Length Fields
Neglected by far too many RPGers
• But becoming more important due to increased usage in Db2
Excellent for string handling
• Reduces requirement for %TRIMx operations
Length is set when data is moved into it
• The field subsequently behaves as if it were fixed length of the current length
The %LEN function can be used to interrogate the length
• And to reset it
Use caution when assigning *Blanks
• The field will be filled with blanks to its maximum length

dcl-s firstName char(10) inz('Jon');


dcl-s lastName char(10) inz('Paris');

dcl-s v_firstName varChar(10) inz('Susan');


dcl-s v_lastName varChar(10) inz('Gantner');

dcl-s fullName varChar(21);

fullName = %trimR(firstName) + ' ' + %trimR(lastName);


fullName = v_firstName + ' ' + v_lastName;

Notes

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 7 of 30


Storage Usage of Varying Fields
Varying length fields have two components
• The current length of the field
✦ A two byte binary - Uns(5)
- Or four byte binary - Uns(10) for fields > 65,535 in length
• This is followed by the actual data
So always two (or four) bytes longer than the defined length
• This is reflected in the results of %Size

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ...

0 1
H E R E I S T H E D A T A
0 6

%Size would
dcl-ds *n; return 2,050
webOut varChar(2048);
webDataLen int(5) overlay(webOut);
webData char(2048) overlay(webOut:*next);
end-ds;

webOut = 'HERE IS THE DATA';

Notes
Variable length fields can give any program that builds a string (for example for a web page, or CSV file) can see
huge performance improvements when varying length fields are used instead of fixed length fields. The reason is
that, as long as you trim a string when it is loaded into a variable length field, you never need to trim it again.
Compare this with fixed length fields where to add to an existing field you have to basically say:
string = %TrimR( string ) + newStuff;

And do this for each new piece of data to be added to the string. The longer the string, the more inefficient this
process is.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 8 of 30


Access Non-Normalized Data as Arrays
Want to access fields in a record as an array?
• The "secret" is to place them in a DS
This works even if the fields are not contiguous in the record
• No length definition is required
• The compiler uses the length supplied by the database
Use the POS keyword to cause the array to overlay the DS
• Notice the use of the LIKE keyword R MTHSALESR
DDS

CUSTOMER 4
STREET 32
dcl-f MthSales;
CITY 24
STATE 2
dcl-ds SalesData; DIVISION 2
Q1; Q1 7 2
Q2; Q2 7 2
Q3 7 2
Q3;
Q4 7 2
Q4; K CUSTNO

SalesForQtr Like(Q1) Pos(1) Dim(4);


end-ds;

Notes
The inspiration for this example comes from a commonly asked question on RPG programming lists: “How do I
directly load fields from a database record into an array?” The question normally arises when handling old databases
that are not normalized. Typically these are from old S/36 or S/38 applications. The type of record I mean contains a
series of related values - for example, sales figures for January, February, ..., December.

To make our example fit on the page we're not going to show 12 months, because it wouldn't fit on the chart!
Hopefully the example of sales figures for four quarters will give you the idea of how it all works. The DDS for the
physical file is shown. One solution is depicted here. We'll look at a slightly different solution on the next chart. Our
objective is to access the individual fields Q1-Q4 as an array of four elements after reading a record from the file.

Notice that we’ve incorporated the Quarterly sales fields into the DS by specifying their names. No length or type
definition is required.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 9 of 30


Naming Indicators
Use names for ALL indicators
• INDDS is RPG's engineered approach
✦ Use it on all new code - more on retrofitting old programs in a moment
dcl-f screen workStn INDDS(displayControls);

dcl-ds displayControls;
F3_Exit ind pos(3);
F12_Cancel ind pos(12); Remember ...
errorIndicators char(3) pos(31);
error ind
startDateError ind
pos(31);
pos(32);
*IN32 means NOTHING!
endDateError ind pos(33);
end-Ds;

errorIndicators = *zeros; // Turn off all error controls


if (startDate < today);
startDateError = *on ; // Start date too early
endIf;

endDateError = ( endDate < startDate ); // End date before start date


error = ( startDateError or endDateError ); // Set error status
exFmt testRec;

if (F3Exit or F12_Cancel);

Notes
The ONLY place where there is an "excuse" for using numbered indicators is in O-specs. And even that is a poor
excuse for new code as you should be using externally described printer files!

The "IBM approved" method of naming indicators is the Indicator Data Structure (INDDS) option for files. But that
only works with externally described files - so program described printer files cannot take advantage of the capability.

Also INDDS creates a separate set of 99 indicators for each structure used. That can be useful but it also means that
INDDS can be hard to use in existing code where *INnn indicators are already in use since indicator 99 in a display
file that uses INDDS is NOT the same thing and *IN99. I'll look at how to address that problem on the next chart.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 10 of 30


Mapping *INnn Indicators to Names
Use a pointer to map the standard *IN indicator array
• An easy way to give names to all of your indicators
• _nn at the end of the name documents actual indicator number
✦ Thanks to Aaron Bartell for introducing us to this variation
I have also used a group field to map the set of error indicators
• Enabling you to clear or set them as a group

dcl-s pIndicators Pointer Inz(%Addr(*In));

dcl-ds displayControls Based(pIndicators);


// Response indicators
Exit_03 Ind Pos(3);
Return_12 Ind Pos(12);
// Conditioning indicators
errorInds_31_33 Pos(31);
Error_31 Ind Overlay(errorInds_31_33);
StDateError_32 Ind Overlay(errorInds_31_33: *Next);
EndDateError_33 Ind Overlay(errorInds_31_33: *Next);
end-ds;

Notes
Note that I used a group field to define the set of indicators 31 - 33 as the single field errorInds_31_33. I could also
have defined it as being Char(3) Pos(30). This allows me to simply code things like Clear errorInds_31_33; I use
this technique for subfile control indicators and use constants with names such as DISPLAYSUBFILE and
CLEARSUBFILE which are defined as patterns of character 1s and 0s as appropriate. Helps to make the code far
more readable.

Unlike the INDDS approach, these named indicators DO directly affect the content of their corresponding *IN
indicator. So, using the above example, if we code Error = *On then indicator *IN30 was just turned on. This often
makes this a better approach for those who use program described (i.e. O-spec) based files rather than externally
described printer files.

Those of you who use the *INKx series of indicators to identify function key usage need not feel left out. A similar
technique can be used. In this case the pointer is set to the address of *INKA. The other 23 function key indicators
are in 23 bytes that follow. IBM have confirmed many times that for RPG IV this will always be the case.

dcl-ds FunctionKeys Based(pFunctionKeys);


F3_Pressed Ind Pos(3);
F12_Pressed Ind Pos(12);
end-ds;

dcl-s pFunctionKeys Pointer Inz(%Addr(*InKA));

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 11 of 30


Templates for Data Structures etc.
Templates are a great way of ensuring standards in the shop
• The same definition is used in all programs
✦ Therefore the same field names, sizes and data types
You can also use them to create your own "data type"
• e.g. A phone number, account number, part number, etc.

dcl-ds baseAddress_T TEMPLATE; I use the _T naming


street1 varchar(50); convention for all
street2 varchar(50; templates.
city varChar(30);
state char(2) inz(‘TX');
zip char(5);
zipPlus char(4);
end-ds;

dcl-ds invoiceInfo qualified;


mailAddress likeDS(baseAddress_T) inz(*likeDS);
shipAddress likeDS(baseAddress_T) inz(*likeDS);
end-ds;

Notes
Templates are an incredibly useful way of ensuring consistency in definitions between programs.

They can also have standard initialization values which will be inherited by any any DS that "clones" them if the
Inz(*LikeDS) option is used.

I use these all the time for defining DS that are used as parameters. being able to simply code LikeDS(xxxxx) on a
parameter definition is such an easy way of ensuring consistency and in the called routine avoids the need to define
the DS as the individual fields can be referenced directly.

The use of _T at the end of the name makes it obvious that my LikeDS reference is to a template. It was a
convention I first encountered in IBM documentation and have used it ever since.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 12 of 30


Handling Error Messages
Traditional Approach
• ERRMSGID in DDS
• Use a Message Subfile
✦ Send messages to program message queue
✦ Message subfile displays messages in program message queue

This approach only works green screen environments


• And that is not where we are headed

You need an approach that is far more versatile


• But could still be used by green screen apps

My colleague Paul Tuohy has published an alternative approach

Handling Error Messages - Paul's Approach


Originally published by ITJungle
• "Getting the Message" Parts 1 and 2
✦ itjungle.com/fhg/fhg101409-story01.html
✦ itjungle.com/fhg/fhg102109-story01.html

Basically it uses a service program that stores messages


• They are stored in an array
✦ But could be stored anywhere
• Subprocedures are supplied to:
✦ Add a message
✦ Clear stored messages
✦ Retrieve a message
✦ Return the number of stored messages
✦ Etc.

On the next chart I'll show you briefly how it is used

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 13 of 30


Using Paul's Message Procedures
This is the basic message layout
• A DS template of course!
dcl-Ds def_MsgFormat qualified template;
msgId char(7);
msgText char(80);
severity int(10);
help char(500);
forField char(10);
end-Ds;

And this is how the APIs are used


dcl-Ds message likeDS(def_MsgFormat) end-Ds;

clearMessages();
addMessage('ERR0001': employeeID: %char(salary));
addMessage('ERR9001': *omit: employeeID);
addMessageText('Bad things happened!!!');
if (messageCount() > 0);
for i = 1 to messageCount();
getMessage( i: message);
// Do what you will with the message
endFor;
endIf;

Notes
If you use a standard method for reporting errors, such as this, you can more easily change the interface.
Use a green screen and the errors can be retrieved and placed on the message queue as before. Use a browser
interface and the messages can be retrieved via Javascript/JSON calls. Use the code in a web service and the error
messages can be wrapped in XML/JSON/whatever.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 14 of 30


Data Structure I/O - Useful in Many Ways
Define the DS using LIKEREC
• DS will have the same format as the named record
✦ Similar to externally described DS but can also be used to define parameters or
nested DS
• Record must be for a file used by the program
✦ Or at least a file defined as a Template
Control of which fields are included is based on the field category
• Specify *INPUT, *OUTPUT, *KEY or *ALL depending on type of operation
✦ When *ALL is used compiler works out which fields to use for input and for output

// Record formats EmpSel, and Select are included in this file


Dcl-F MSTDSP Workstn;

Dcl-ds SubFileRecs LikeRec( EmpSel : *OUTPUT ) Dim(20);

Dcl-ds DisplayData Qualified;


SelectInp LikeRec( Select : *INPUT )
SelectOut LikeRec( Select : *OUTPUT

Notes
DS I/O offers many advantages not the least of which (as with all qualified references) being that there is never any
doubt as to the source of the data.

Another benefit is that you can keep multiple copies of a record in the program and easily compare the same field in
multiple records.

In the example that follows we are only going to be comparing the record at the global level - but we could of course
compare on a field by field basis. For example, you could compare the record image and if there was a mis-match go
ahead and compare individual fields to see if the changed fields impact the planned update. If the field changed
between the initial read and subsequent access is not one that was changed by the user then perhaps it is fine to go
ahead and perform the update.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 15 of 30


Avoiding Record Locks Using DS I/O
These are the basic file and record definitions for my sample
• I used IndDs for the display file as an alternative to mapping *In
• Record image for the customer file is defined as a 2 element array
• Image for display file will be used for ExFmt so it also uses *All
For a more detailed description see my article in RPG Guru
• itjungle.com/fhg/fhg030315-story01.html
Dcl-F TestFile Usage(*Update:*Delete) Keyed;
Dcl-F CustUpdd WorkStn IndDS(displayInds);

Dcl-Ds displayInds;
exit_03 Ind Pos(3);
update_06 Ind Pos(6); // Request Update of Record
canUpdate_96 Ind Pos(96); // Update option available
End-Ds;

Dcl-Ds custRec Likerec(TestRec: *All) Dim(2);

Dcl-Ds displayRec LikeRec(Disprec: *All);

Notes
Too often master file update programs are written that pay no attention to the record lock created. Locking the record
while the user makes changes is not a problem - as long as the user doesn't stop to take a phone call in the middle
of the update resulting in a 5 minute lock. Even worse is the case where they wander off to grab a coffee, meet a
colleague and chat for 10 minutes. That lock is just a little landmine waiting for some other application to trip over it.

The process used here involves simply reading the record without a lock initially and storing an image of the record.
Once the user has made the changes and submits the update the record is read again (this time with a lock) and the
new record image compared with the one stored. If the two match the update can proceed - if not the user must be
notified, shown the current data, and invited to try again.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 16 of 30


Avoiding Record Locks ... - Main Loop
Main logic is in LoadRecord() and ProcessUpdate() procedures
• This is just the main driver logic
Note the use of the displayRec DS with the ExFmt operations

ExFmt DispRec displayRec; // Output initial display

DoW Not exit_03; // Continue until user says to exit


If update_06;
ProcessUpdate();
ElseIf exit_03; // Exit when requested
*InLR = *On;
Leave;
Else; // Treat as a new request and load the data
LoadRecord();
EndIf;
ExFmt DispRec displayRec;
canUpdate_96 = *Off;
EndDo;

Return;

Avoiding Record Locks ... LoadRecord()


Notice the use of the N(o lock) extender
• The record will be displayed without being locked
The record is loaded into element 1 of the array
• That image is then used in Eval-Corr to load the display record

Dcl-Proc LoadRecord;
Dcl-Pi *N End-Pi;

Chain(N) displayRec.ARCode TestFile custRec(1);

If %Found(TestFile);
Eval-corr displayRec = custRec(1);
displayRec.message = 'Press F6 to update customer +
or <Enter> to view new customer';
canUpdate_96 = *On;
Else;
displayRec.message = ('** Error: Customer ' + displayRec.ARCode +
' not found **');
EndIf;

End-Proc LoadRecord;

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 17 of 30


Avoiding Record Locks ... ProcessUpdate()
Record is read into element 2 of the array - this time record is locked
• It is then compared with element 1 (the original) to check for changes
If no changes then the update proceeds
✦ Using the data from the display record to update element 1
✦ Then that element is used as the source data for the Update operation
If a change was detected
• Operator is notified and the latest record is set as the current one
Dcl-Proc processUpdate;
Dcl-Pi *N End-Pi;

Chain displayRec.ARCode TestFile custRec(2); // Read and lock record


If custRec(1) = custRec(2); // Compare latest record with original
Eval-corr custRec(1) = displayRec; // Update record image
Update TestRec custRec(1); // and apply the update
displayRec.message = ('Updated Customer ' + displayRec.ARCode);
Else;
displayRec.message = ('** Error: Customer ' + displayRec.ARCode +
' had been updated - please try again.');
Eval-corr displayRec = custRec(2); // Reset display screen
custRec(1) = custRec(2); // and set image 1 to current record
canUpdate_96 = *On;
Unlock TestFile;
EndIf;

Notes
After the article I wrote on this topic appeared in IT Jungle a reader wrote in and queried my use of a DS array to
hold the record images. He preferred to use two different DS, each based on the same record layout. But unlike my
example that uses *All, in his case he defines one using the input layout and the other the output - which of course
are normally the same thing for a database but RPG can still be fussy about using the right one for the right task.

For him this was less confusing. Perhaps you feel the same but for me I prefer to use the array approach for two
reasons.
First using an array makes it very obvious that the two structures being compared are identical. With the two DS
approach you no assurance that both DS used the same definition.
Secondly, the approach I use is an evolution of an earlier approach that used a Multiple Occurrence DS (MODS) to
achieve the same thing. I felt that anyone familiar with that approach would find this one easier to follow.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 18 of 30


MONITOR for Errors
Monitor allows you to trap any errors that occur in a block of code
• Block can range from a single line to the entire program
Can be used for all operations
• Even those that do not support the (E) error extender
Similar in some ways to CL's MONMSG
Errors that are not caught will behave as normal
• i.e. Get caught by the PSSR, or cause the green screen of death

Monitor;
Read Customer;
If Not %EOF(Customer);
line1 = %SUBST(inputData : %SCAN('***': inputData) + 1);
EndIf;
On-Error 1211; // << Use named constants - but you can hard code
// ... handle file-not-open
On-Error 00100;
// ... handle string error and array-index error
On-Error;
// ... handle all other errors
EndMon;

Notes
A monitor group monitors all of the code between the Monitor operation and the first On-Error operation.
If an error is detected, then control passes to the first On-Error operation etc. If it matches the condition specified
there the specified action is taken and the exception considered to have been handled. If the condition is not
matched then the next On-Error is checked and so on until either the exception has been handled or it is determined
that there is no action specified. At that time the normal RPG exception handling will kick in.
In addition to specific error code values, the special values of *PROGRAM, *FILE and *ALL can be specified.

On-Error; and On-Error *All; are equivalent.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 19 of 30


MONITOR for Errors
That first example used numeric literals for the error codes
• DON'T DO THAT !
• Use constants with meaningful names
A standard set can easily be incorporated via a /COPY
• Feel free to email me for copies of the ones I use
✦ An extract is shown on the Notes page

Monitor
Read Customer;
If Not %EOF(Customer);
line1 = %Subst(inputData : %Scan('***': inputData) + 1); EndIf;
On-Error FILEISCLOSED;
// ... handle file-not-open
On-Error STRINGRANGEERR;
// ... handle string error e.g. line1 = *Blanks
On-Error *All;
// ... handle all other errors
EndMon;

Extracts From a Sample Status Code File

// Value out of range for string operation


Dcl-C STRINGRANGEERR 00100;
// Variable-length field has a current length that is not valid.
Dcl-C FIELDLENERR 00115;
// Table or array out of sequence.
Dcl-C ARRAYSEQERR 00120;
// Array index not valid
Dcl-C ARRAYINDEXERR 00121;
// OCCUR outside of range
Dcl-C INVALIDOCCUR 00122;
// I/O operation to a closed file.
Dcl-C FILEISCLOSED 01211;
// OPEN issued to a file already open.
Dcl-C FILEISOPEN 01215;

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 20 of 30


A Different Approach to Program Errors
This example shows the traditional way of doing things
• Often code like this has a change marker on it
✦ i.e. the idea that stock would ever be zero was not considered by the
original programmers

Dcl-S QtyInStock Packed(5);


Dcl-S CostOfGoods Packed(9:2);
Dcl-S AverageCost Packed(9:2);

// The "Old" way


If QtyInStock <> 0;
AverageCost = CostOfGoods / QtyInStock;
// and other related calcualtions
Else;
AverageCost = 0; // etc. etc.
EndIf;

Notes
One of the problems with this type of code is that we often leave future maintenance programmers wondering just
why we were testing for zero. They may not really find out for a page or two - particularly if viewing the program
using the limited visibility of SEU - i.e. a mere 18 lines at a time.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 21 of 30


A Different Approach to Program Errors
This is my preferred approach because ...
• It is far more efficient if a stock level of zero is a rare situation
• It makes the expected execution path more obvious
• It is also easy to add "just in case" logic to a block of calculations

Dcl-S QtyInStock Packed(5);


Dcl-S CostOfGoods Packed(9:2);
Dcl-S AverageCost Packed(9:2);

Dcl-C DIVBYZERO 102;

// The New Way


Monitor;
AverageCost = CostOfGoods / QtyInStock;
// and other related calculations
On-Error DIVBYZERO;
AverageCost = 0; // etc. etc.
EndMon;

Notes
The nice thing about this approach is that our main line logic doesn't become cluttered by "we hope it never happens
but ..." type code.
If we wished to trap separately for other possible types of error, we could simply add more ON-ERROR operations
together with their associated code. Don't forget that by coding ON-ERROR *ALL (or simply leaving the extended
factor2 field blank) we can supply catch-all coding for any other error that may occur. As the example is written, any
other error will simply blow up with the normal two-line "screen of death" or trigger the PSSR if one is present.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 22 of 30


Another Example - Using DS I/O
Here DS I/O is used to avoid decimal data errors (DDEs) on a Read
• So errors only occur when and if the field is used
• You can then programmatically deal with the error or report it
DS I/O can resolve Decimal Data Errors temporarily
• At least until you can fix the program writing the "bad" data in the first place

Dcl-F MyFile;
Dcl-DS inputData LikeRec(Record1);

Dcl-S DEC_DATA_ERR 907;

Read Record1 InputData; // Read directly into DS - so no data error

Monitor;
Num1 = InputData.InpNum1;
Num2 = InputData.InpNum2;
Num3 = InputData.InpNum3;

On-Error DEC_DATA_ERR;
// Place code here to react to Dec Data Errors
EndMon;

Notes
A few things to note about this example: First, it wouldn't be sufficient to simply put in the MONITOR code without
also adding the DS name to the READ operation. We need to avoid the possibility that a Decimal Data Error could
be triggered on the read itself as the data is moved to its internal storage area. Using DS I/O prevents the error from
being signalled on the READ because the database record is moved as a single large character field to the data
structure named InputData.

In order to ensure that the DS exactly matches the layout of the record buffer, we are taking advantage of the
LIKEREC keyword. When we use LIKEREC the compiler guarantees that the layout of the DS exactly matches the
layout of the record buffer.

Because of the use of the LIKEREC keyword, the DS implicitly becomes a Qualified DS. Therefore we use the DS
name qualification syntax to specify we want the data from the DS fields. Note that because of the DS I/O "normal"
fields (e.g. InpNum1, InpNum2, etc.) will not contain the data from the record. We bypassed those fields by reading
the data directly into the DS .

Of course, the code to report and fix the errors can be challenging in an example like this, since there are 3 possible
fields in error. If you only plan to report that the record as a whole is in error, this code will do the job. But if you
want to attempt to identify the specific field in error and take corrective action you will need to do a little more work.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 23 of 30


Take Advantage of I/O Enhancements
KLISTs are so 1900s!
• %KDS is a free-form replacement for KLIST
• Used in Factor 1 position instead of a KLIST name
Syntax:
%KDS(keyDSName { : numberOfKeysToUse } )
• The Optional 2nd parameter specifies # of fields to be used in partial key
All keyed operations can use this new BIF
• Automatically generate required DS by using LIKEREC(recname : *KEY)

// The ProductKeys DS contains the three fields that make up the


// key for the Product File (Division, PartNumber and Version)
Dcl-DS ProductKeys LikeRec(ProductRec : *Key);

// Read a specific record using full key


Chain %KDS(ProductKeys) ProductRec;

// Position file based on first 2 key fields


SetLL %KDS(ProductKeys : 2 ) ProductRec;

Notes
Until this feature was introduced, there were two ways to specify the key for a keyed operation. Specify the name of
a single field or the name of a KLIST.
KLISTs always annoyed me because you had to wander off elsewhere in the program to actually find the list. Only
then did you know what keys were being used. Not only that, but if you wanted to be able to use all keys, or two
keys, or ... then you needed a separate KLIST for each set of keys.
This new support offers both an improved alternative to the KLIST approach and a new method of directly specifying
the keys on the operation itself.

The new "KLIST" (actually a BIF called %KDS - Key Data Structure) references key definitions in the D specs where
they belong. It uses the *KEY option of LIKEREC. You can use this to automatically generate a DS containing the
file's key fields. This structure can then be referenced in the I/O operation by specifying the DS name to the new
%KDS function.

So how do you specify that a partial key is to be used? Just use the second parameter of %KDS to tell the compiler
how many of the key fields are to be used.

We will look at the second method on the next chart.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 24 of 30


Take Advantage of I/O Enhancements (Contd.)
Keys can now be simply specified as a list of values
• List is specified in Factor 1 position, and enclosed in parentheses
Any type of field, literal or expression can be used
• As long as the base type matches AND the key(s) are in parentheses
• Compiler performs any required conversions
✦ Using the same rules as for EVAL operations
• Note that the absence of parentheses changes the behavior
✦ See final example below
// Read using specified keys
Chain (Division : PartNumber : Version) ProductRec;

// Position file based on first 2 key fields


SetLL (Division : PartNumber) ProductRec;

// Position file to specified Part number in Division 99


SetLL ( '99' : PartNumber) ProductRec;

Chain custNum Record; // custNum must exactly match attributes of key


Chain (Field) Record; // custNum only needs to match key's base type

Notes
The second method is an extension of the current ability to specify a single field as the key (the old Factor 1).

Instead of a single field, you can now supply a list of fields. The list should be specified within parentheses with
colons (:) used to separate the individual key elements.

Note that the key elements do not have to be fields, they can be any character expression. The compiler will
perform conversion if required.

Note that even if you have a single field key, it is often better to enclose it in parentheses because with the
parentheses the field specified is not required to match exactly in type and size, but without parentheses they must
match. This is much like the size & type accommodation made by the compiler with the use of the CONST keyword
in prototypes for parameters.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 25 of 30


Update in Safety - %FIELDS
Which fields in the file does the first UPDATE modify?

Can you rely on the comment ?

With %Fields you have a 100% guarantee


• Only the requested fields will be updated

Dcl-F Product Keyed Usage(*Update);

// OK - now we update the Unit Cost and Price fields


Update Productl;

// Update only UnitCost and UnitPrice fields


Update Product %Fields( UnitCost : UnitPrice );

Notes
In many ways this is both one of the best of the new I/O features and one of the most under utilized. You should be
using this in EVERY program that is supposed to only perform updates on specific fields.

It provides a great way to protect your code from the worst efforts of (shall we say) "less-gifted" programmers. The
list of fields is specified using the new BIF %Fields. Only those fields specified will be updated.

Why is this so useful? Suppose that, during the operation of the program, only certain fields in the file should be
subject to change. By specifying those fields to the UPDATE op code, you are assured that only those fields will be
changed. If during subsequent maintenance tasks a mistake is made and the value of a field that should not be
modified is accidentally changed in the code, it will have no effect on the database. Only if the %Fields list is also
modified can this error result in database corruption.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 26 of 30


Use Short-form Expression Support
A lot of RPGers really don't like this
• They're wrong ...
It is great when using long/subscripted/qualified names
• Particularly if like me you are are a poor typist !!
• And it is a notation style that will be familiar to newcomers
Any math or string expression can be shortened
• e.g. X = X + 1; becomes X += 1;
✦ += is used for addition or string concatenation
✦ -= for subtraction
✦ and you can guess what the others are ...

// This calculation
MonthTotal = MonthTotal + TotalSale;
// can be shortened to this
MonthTotal += TotalSale;

// Be careful though! =+ and += are not the same


x =+ 1; // This is the same as x = 1;

Notes
Numeric operations now support short-form notation for certain functions. Prior to this release, an addition of the
type X = X + 1 required that you repeat the name of the target field. Some people considered this a step backwards
since the old ADD op-code offered a short-form notation that only required the target field to be specified once, in the
result field. e.g. ADD 1 X.

With this new feature, the expression can be written as X += 1. Similar shorthand can be used for subtraction,
multiplication, division, and exponentiation.

Do not make the mistake of coding it like this: X =+ 1 This simply puts a positive value of 1 into X.

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 27 of 30


Short Form Expressions
Still Don't like it ?

* Which version would you have coded ?


C MonthTotal ADD TotalSale MonthTotal

C ADD TotalSale MonthTotal

Why not ?

• It is just a straight reversal - just like all other free-form options !


✦ Result comes first
✦ Followed by operation
✦ And then factor 2 !

MonthTotal += TotalSale;

Always Use Offsets !


Many IBM supplied programs use special “buffers”
• Many APIs
• Trigger buffers
If documentation mentions an offset – USE IT!
• Do NOT hard code positions
• Bad things can happen
An example
• Prior to V5R1 many programmers hard coded positions for before/
after images in the trigger buffer
• In V5R1, IBM changed the rules for the buffer
✦ Each image is now aligned on a 16 byte boundary
• So you only had a 1 in 16 chance that your layout was correct !
• “Lucky” programmers just got a decimal data error
✦ Others had their programs "run" - but the results ...

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 28 of 30


Using Offsets - Tigger Buffers
The layout of the actual buffer is shown on the notes page
• This code sample just shows how the offsets are used

// Before and After Record Images


Dcl-DS OldRecord ExtName('YOURFILE') Qualified Based(pOldRecord)
End-DS;
Dcl-DS NewRecord ExtName('YOURFILE') Qualified Based(pNewRecord)
End-DS;

Dcl-Pi play ExtPgm;


triggerBuffer LikeDS(triggerBuffer_T);
triggerBufferLength Int(10);
End-Pi;

// Map record image DS to Trigger Buffer using Pointers


pOldRecord = %Addr(triggerBuffer) + triggerBuffer.oldOffSet;
pNewRecord = %Addr(triggerBuffer) + triggerBuffer.newOffSet;

// Any Trigger Logic needed goes here . . .

Notes
This is a basic template for the Trigger Buffer parameter.

The *N fields are reserved filler areas and are not currently used.

Dcl-DS TriggerBuffer_T Template;


FileName Char(10);
LibraryName Char(10);
MemberName Char(10);
Event Char(1);
Time Char(1);
CommitLock Char(1);
*N Char(3);
CCSID Int(10);
RRN Int(10);
*N Int(10);
OldOffset Int(10);
OldLength Int(10);
OldNullOff Int(10);
OldNullLen Int(10);
NewOffset Int(10);
NewLength Int(10);
NewNullOff Int(10);
NewNullLen Int(10);
End-DS;

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 29 of 30


That's All Folks !

Any Questions ?

© Partner400, 2018 RPG Tips and Techniques Lecture Notes: Page 30 of 30

You might also like