I am not sure if the title really describes what this post is about. The germ for this post came from an email I received from Elbert Cook. He asked me about emulating output specifications in a modern RPG program. Modern free format RPG does not support output specifications. I am pleased about this. I can get far more functionality and make changes many times faster by creating my own printer file than I would making changes to an output specification. Some have argued to me that printer files are too new for them to use, which is not true as I first used them when this operating system was OS/400 and it was running one of the releases of version 2.
Let me stress that what I describe in this post is NOT a case for creating something like output specifications. Use custom printer files.
There are times where I have created printed output from a program I was testing or developing that was so "down and dirty" I did not want to spend my time creating a printer file for. This post is based upon the methods I have used, and some examples from Elbert.
The scenario I am working with is to give a way I can print a list of all the state codes and their names from the State Master file. This is the first post in a long time where I am going to use the RPG read operation code, rather than SQL.
I am going to show the source code for my program in three parts. The first part are the definitions:
01 **free 02 ctl-opt dftactgrp(*no) ; 03 dcl-f STATECODE keyed extfile('TESTLIB/STATCDE') extdesc('TESTLIB/STATCDE') ; 04 dcl-f QSYSPRT printer(132) oflind(PageOverflow) prtctl(PrtCtlDS) ; 05 dcl-s PageOverflow ind ; 06 dcl-s Count uns(5) ; 07 dcl-ds PrtCtlDS qualified len(15) ; 08 SpaceBefore char(3) inz(' 0') ; 09 SpaceAfter char(3) inz(' 1') ; 10 SkipBefore char(3) inz(' 0') ; 11 SkipAfter char(3) inz(' 0') ; 12 end-ds ; 13 dcl-ds Output qualified len(132) ; // Headings 14 Title char(80) pos(1) ; 15 PageWord char(4) pos(82) ; 16 PageChar char(4) pos(86) ; // Details 17 State char(2) pos(5) ; 18 Name char(30) pos(11) ; 19 end-ds ; 20 dcl-c PageHeading 'Title for this test report' ; 21 dcl-c ColHeading1 ' Code State name' ; 22 dcl-c ColHeading2 ' ---- ------------------------' ; |
Line 1: This program has to be in modern totally free format RPG.
Line 2: As I use a subprocedure, rather than a subroutine, I need this control option.
Line 3: This is the file definition for the State Master file. The default usage for data files is input only, therefore, I do not have to use the USAGE keyword. I want to read the file in keyed order. The State Master file is not in standard library list, and I have decided to use the following two keywords that allows me not to add the library it is in into my library list. I have given my name for the file definition to be STATECODE, even though this is not the name of the file. I can "override" the name and location of the real file using the EXTFILE keyword. When the program is called the file given in the EXTFILE is opened. How do I compile the program without the file being in my library list? I use the EXTDESC keyword to tell the compiler which file to use when compiling. In this case both of these parameters contain the same file in the same library.
Line 4: This is the definition of my printer file. As this is a printer file the default usage is output only. QSYSPRT is one of IBM i's default printer files and I need to give a record length for my spool file, which I do using the PRINTER keyword. I need to give an indicator that will come "on" when the spool file overflows onto a new page. I can use a named indicator I create for this purpose, which I think is many times better than a number indicator or the default overflow indicators. I also need to have a Print Control data structure for the line feed options. Here I use the PRTCTL keyword, and within it I give the name of my printer control data structure.
Line 5: I need to define the indicator I will be using to control page overflow.
Line 6: When testing I always like to have a count of the number of records I have printed, and this unsigned integer is for that purpose.
Lines 7 – 12: This is the definition for the printer control data structure. I need this as this is where I "tell" the printer file when and how much to line feed when I write a record to the spool file. Those of you familiar with output specifications will recognize the names I have given the subfields, and what they numbers mean.
Lines 13 – 19: I always have my printer variable as subfields in a data structure. I find this is the easiest for me to move values into the columns. Being a data structure there is no reason I cannot have subfields overlaying one another. In this example lines 14 – 16 are the subfields for printing the page headings, and lines 17 – 18 are the repeating details.
Lines 20 – 22: These are three constants that hold the text for the spool file's page headers.
Next snippet of the code is the "main body" of the program:
23 NextPage() ; 24 dow (*on) ; 25 read STATECODE ; 26 if (%eof) ; 27 leave ; 28 elseif (PageOverflow) ; 29 NextPage() ; 30 endif ; 31 Output.State = STCODE ; 32 Output.Name = STNAME ; 33 write QSYSPRT Output ; 34 Count += 1 ; 35 enddo ; 36 Output.Title = ' Record count = ' + %char(Count) ; 37 write QSYSPRT Output ; 38 *inlr = *on ; |
Line 23: The NextPage subprocedure is what writes the headings. I will describe how that does that in the next snippet.
Line 24: This is going to be a "never ending" loop. The only way to leave it is with a LEAVE operation code.
Line 25: I read the State Master file.
Lines 26 and 27: If the end of file is encountered processing leaves this Do loop.
Lines 28 and 29: If the page overflow indicator is "on" then I call the NextPage subprocedure.
Lines 31 and 32: I move the contents of the Sale Master file's fields into the subfields of the data structure.
Line 33: I write to QSYSPRT using the data in the Output data structure. In this situation that is the state code and name.
Line 34: As I want to have a count of the number of records I have written to the spool file, I need to increment my count.
Line 36 and 37: When I leave the Do loop I want to write the count to the spool file. I use the title subfield in the Output data structure to contain the information. When I write to QSYSPRT and the information from the Output data structure is written to the spool file.
The last snippet is the NextPage subprocedure:
39 dcl-proc NextPage ; 40 dcl-s PageNumber uns(3) static ; 41 if (PageOverflow) ; 42 PrtCtlDS.SkipBefore = ' 1' ; 43 endif ; 44 Output.PageWord = 'Page' ; 45 PageNumber += 1 ; 46 evalr Output.PageChar = %char(PageNumber) ; 47 Output.Title = PageHeading ; 48 write QSYSPRT Output ; 49 clear Output ; 50 if (PageOverflow) ; 51 reset PrtCtlDS ; 52 PageOverflow = *off ; 53 endif ; 54 Output.Title = ColHeading1 ; 55 write QSYSPRT Output ; 56 Output.Title = ColHeading2 ; 57 write QSYSPRT Output ; 58 clear Output ; 59 end-proc ; |
The thing to remember with this subprocedure is the first time it is called, on line 23, the overflow indicator is not "on".
Line 40: The page number only needs to be local to the subprocedure. Notice I have the STATIC keyword with it. What this does is preserve the value in PageNumber from the last time the subprocedure was called to the next, and it can be incremented to the next page number. If I did not have this keyword the page number would always be '1'.
Lines 41 - 43: When the page overflow indicator is "on" I change the subfield in the Print Control data structure to skip to the first line of a new page before writing to the spool file.
Lines 44 – 46: This is where I move the description to the Pages fields, increment the page count, and convert it to character for the Output data structure subfield.
Line 47: I move the page header constant to the title subfield in the data structure.
Line 48: When I write to QSYSPRT I am writing the contents of the Output data structure, if this is a page overflow situation it takes into account the contents of the Printer Control data structure and skips to the start of the next page.
Line 49: I clear the contents of the data structure. I decided this was better as it is less lines of code than if I cleared the page name and number subfields. If I did not then the page name and number would be repeated on the next lines.
Lines 50 – 53: If this is a page overflow situation.
Line 51: I use the RESET operation code to return the value of the PrtCtlDS data structure to what it was at program initialization.
Line 52: As the page overflow indicator is "on" I change it to "off".
Lines 54 – 57: This is where I write the column heading lines to the spool file.
Lines 58: As this is the end of the subprocedure I clear the Output structure to ensure that when I use it for the details there will be no "bleed over" from the headings.
After compiling the program I call it, and my spool file looks like:
Title for this test report ... Page 1 Code State name ---- ------------------------ AK Alaska AL Alabama AR Arkansas Title for this test report ... Page 2 Code State name ---- ------------------------ WV West Virginia WY Wyoming Record count = 51 |
As I said at the start of this post what I have described is NOT a way to create pseudo-output specifications. You should be using printer files for your spooled output. If you have output specifications in your programs and you despair of converting them to printer files there are non-IBM tools that will do this for you. You should use them and move over to modern free format RPG.
Thank you to Elbert for sharing his code with me.
This article was written for IBM i 7.5, and should work for some earlier releases too.
Thanks Simon, I like the idea of getting rid of o specs and this solution lets us do so while still retaining all the logic inside our rpg. No need to go see what the prtf is doing.
ReplyDeleteThat was not the intention of this post.
DeleteIMHO we should be using printer file for any production reports.
Why? I can move things around in a printer file or not have the same layout for two departments/divisions/etc. and do it without the need to recompile the RPG program.