Jon Paris
Jon.Paris @ Partner400.com www.Partner400.com www.SystemiDeveloper.com
RPG Tips and Techniques
NotesAbout 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
AgendaBasic 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 ThoughtsBe 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 ThoughtsLearn 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-OptThese 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 OptionsMany 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’);
NotesMultiple 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 DirectivesCompiler 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 IntegersTrue 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 DetailsIntegers 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 JavaINT(3) 1 -128 to 127 BIN1 charINT(5) 2 -32,768 to 32,767 BIN2 shortINT(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 9,223,372,036,854,775,807
BIN8 long
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 longUNS(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 FieldsNeglected 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 FieldsVarying 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
dcl-ds *n; webOut varChar(2048); webDataLen int(5) overlay(webOut); webData char(2048) overlay(webOut:*next); end-ds;
webOut = 'HERE IS THE DATA';
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ...
00
16 H E R E I S T H E D A T A
%Size would return 2,050
NotesVariable 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
DDS
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
dcl-f MthSales;
dcl-ds SalesData; Q1; Q2; Q3; Q4;
SalesForQtr Like(Q1) Pos(1) Dim(4); end-ds;
R MTHSALESR CUSTOMER 4 STREET 32 CITY 24 STATE 2 DIVISION 2 Q1 7 2 Q2 7 2 Q3 7 2 Q4 7 2 K CUSTNO
NotesThe 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 IndicatorsUse 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); errorIndicators char(3) pos(31); error ind pos(31); startDateError ind pos(32); 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);
Remember ...
*IN32 means NOTHING!
NotesThe 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 NamesUse 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;
NotesNote 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; street1 varchar(50); street2 varchar(50; 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;
I use the _T naming convention for all
templates.
NotesTemplates 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 MessagesTraditional 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 ApproachOriginally 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 ProceduresThis is the basic message layout • A DS template of course!
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;
dcl-Ds def_MsgFormat qualified template; msgId char(7); msgText char(80); severity int(10); help char(500); forField char(10); end-Ds;
NotesIf 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 WaysDefine 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
NotesDS 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);
NotesToo 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 LoopMain 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;
NotesAfter 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 ErrorsMonitor 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;
NotesA 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 ErrorsThat 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 ErrorsThis 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;
NotesOne 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 ErrorsThis 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;
NotesThe 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/OHere 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;
NotesA 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 EnhancementsKLISTs 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;
NotesUntil 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
NotesThe 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 - %FIELDSWhich 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 );
NotesIn 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 SupportA 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;
NotesNumeric 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 ExpressionsStill Don't like it ?
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;
* Which version would you have coded ? C MonthTotal ADD TotalSale MonthTotal
C ADD TotalSale MonthTotal
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 BuffersThe 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 . . .
NotesThis 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