Natural Tips & Techniques Natural Tips & Techniques This page is still in its infancy, but I plan to publish suggestions, code samples, even utilities, on a regular and frequent basis (regular and frequent being relative terms). If you have any comments on the content or layout of this page please let me know. Tip Date Published Description 9 02/28/1998 Natural source comparisons 8 11/05/1997 Monthly date calculations 7 11/05/1997 Dynamic report distribution 6 07/14/1997 Natural batch restartability 5 07/07/1997 Sequential match 4 07/07/1997 READ syntax 3 07/05/1997 Replacing certain AT BREAKs with a SORT Continuation Symbol 2 06/30/1997 Here are some uses you may not have thought of. 1 06/10/1997 Initialization of an SPDE Tips are listed chronologically, in reverse to let you find the more recent ones quickly. Return to Ralph G. Zbrog's Home page. Tip 9 02/28/1998 Compare Natural source There is no need to purchase additional software products to compare two Natural source members if you have MVS/TSO. Just download ZZUTILs. Then use the ZPUNCH utility to extract Natural source to a flat file, and IBM's SuperC comparison utility to tell you what the differences are. ZZCOMPR is a Natural program which submits a batch "compare" job from on-line via SAG's NATRJE facility. If you need to submit the jobs manually from TSO, extract the embedded JCL from ZZCOMPR and customize it as necessary. Top of Tips & Techniques. Previous tip. Top of this tip. Return to Ralph G. Zbrog's home page. Tip 8 11/04/1997 Monthly date calculations Often we are called upon to compute dates which are one, two, and three months into the past, for aging report purposes. Sometimes we need to know how many individuals in a file will reach their twenty-first birthday within the next six months. In these and countless other situations, date calculations are being made based on a number of months. For a quick estimate, we could multiply the number of months by thirty and then add or subtract this number from a D-format value. For a more accurate computation, use ZZMONTH. ZZMONTH is a Natural subprogram. Pass it a date and a number of months and it returns a new date and a date range. The new date will be in the future or past depending on whether the number of months is positive or negative, respectively. The date range will be the first day of the month and last day of the month with respect to the new date. For example CALLNAT 'ZZMONTH' #GIVEN #MONTHS #EXACT #FROM #THRU If #GIVEN contains D'11/04/1997' and #MONTHS contains 3, upon return #EXACT will contain D'02/04/1998', #FROM will contain D'02/01/1998', and #THRU will contain D'02/28/1998'. If #GIVEN contains D'11/04/1997' and #MONTHS contains -3, upon return #EXACT will contain D'08/04/1998', #FROM will contain D'08/01/1998', and #THRU will contain D'08/31/1998'. The source for ZZMONTH includes an on-line mainline to help you test the subprogram. Try setting #GIVEN to D'11/30/1995' and #MONTHS to 3. Then change #GIVEN to D'11/30/1997' or D'02/28/1997'. Top of Tips & Techniques. Most recent tip. Previous tip. Top of this tip. Next tip. Return to Ralph G. Zbrog's home page. Tip 7 11/04/1997 Report Distribution Say you have a report which has a page break by Region Code. Each region's pages should be printed on that region's printer. In addition the head office wants a complete set of the regional report. Prior to the DEFINE PRINTER statement it was common to write one program for head office and one for each region, or to break the data by region into subset datasets, reporting each dataset individually. With the DEFINE PRINTER statement you need pass the data only once. Use WRITE(xx) for the head office report (invariably I use WRITE(01) for this guy); you don't need a DEFINE PRINTER(xx). Use WRITE(yy) for the regional reports (WRITE(02) seems pretty logical to me). Create a subroutine to determine dynamically to which printer to route the current regional report. Perform the routine at start of data (don't assume the first data record is for the first region) and at break of region. RESET *PAGE-NUMBER(yy) DECIDE ON FIRST #REGION VALUE 'a1' DEFINE PRINTER(yy) OUTPUT 'CMPRT02' VALUE 'b2' DEFINE PRINTER(yy) OUTPUT 'CMPRT03' ... In your JCL, CMPRT02 and CMPRT03, etc, will point to specific regional printers. You will want the WRITE(yy) TITLE statement to contain the word REGIONAL while the WRITE(xx) TITLE statement reflects its broader scope. There is no need for a NEWPAGE(yy) statement. Execution of the DEFINE PRINTER implies a new page. The only difference in the detail line WRITE statements is the printer designation. The field lists are identical. Of course you still have a problem if you need more than a total of 31 report destinations. That's Natural's limit. You can see sample code and JCL here. I would like to thank Ray Bergen for this tip. Although I knew it was possible to do this in theory, Ray was the one who came up with the right combination of PERFORM, DECIDE, and OUTPUT specifications. Top of Tips & Techniques. Most recent tip. Previous tip. Top of this tip. Next tip. Return to Ralph G. Zbrog's home page. Tip 6 07/14/1997 Natural Batch Restartability ZZRESTRT is a source module which demonstrates how to code a restartable Natural program. It is only a stub, requiring significant customization. Optional code is identified with comments. ZZRESTRT includes the following features: It ensures a correct ET (END TRANSACTION) data layout by including the program name, key value, record count, etc. It demonstrates the need for a sequence number to uniquely identify WORK records created during execution. It includes the ADABAS ISN in ET data to differentiate records with a nonunique key. It contains optional trace statements for debugging purposes. It allows the programmer to refresh program status (clear the ET data) for debugging purposes. It demonstrates how to determine whether the program is starting or re-starting. Note that ZZRESTRT does not produce a formatted report. This is because all reports would be fragmented under restart conditions. Rather than present a user with a production report in two or more discontiguous portions, all report data are written to a work file. Following the update step the work data set is sorted and reported, producing a single, contiguous report. When a program ABENDs, all updates which have not been committed are backed out automatically, but all work records remain on the sequential output data set. When the program is restarted, the backed out transactions are re-processed and duplicate work records are created (in the JCL the new records are appended to the original data set). When the data set is sorted for reporting, as suggested above, the SEQuence field is used to eliminate the duplicates. In DFSORT and SYNCSORT this is done by including the SEQuence field in the low order of the sort and including SUM FIELDS=NONE. Also note the placement of the final WRITE WORK and summary audit record prior to the final ET. This placement is intentional. Even if the program ABENDs after the last ADABAS record has been processed, program execution will be in restart mode and the final totals will be correct. Top of Tips & Techniques. Most recent tip. Previous tip. Top of this tip. Next tip. Return to Ralph G. Zbrog's home page. Tip 5 07/07/1997 Sequential match When programmers move from a 3GL to Natural, one of the first techniques to go is the sequential match - the process of reading two files (invariably a master file and a transaction file) sequentially, comparing their keys, and processing matches, widows (masters with no transactions), and orphans (transactions with no master). There are two reasons for this. First, it is just too easy to code a READ of the master file with a nested READ or FIND of the transaction file. Second, it is not possible in Natural to code a true sequential match of two ADABAS files, because the second loop must be closed before the outer loop can be reiterated. The best you can do is code a step-sequential match (as was already described as a READ with a nested READ or FIND). The step-sequential method has several problems: The READ MASTER/FIND TRANSACTION will not detect orphans; a separate READ TRANSACTION/FIND MASTER is necessary. Initiating a READ or FIND loop is much more expensive than a GET NEXT process. The READ of the MASTER is initiated only once, but the FIND is initiated once for each MASTER record. For moderate or substantial MASTER volumes, resources requirements can be very high. The solution is to process the files as a true sequential match, reading each file only once from start to finish. This is done by extracting the transaction file to a WORK file. The READ of the WORK file is nested within the READ of the MASTER file. The benefits of this approach: The transaction file is processed in a single invocation of a READ, possibly PHYSICAL, with PREFETCH/MULTIFETCH. If no other ADABAS accesses are in the sequential match program, the MASTER READ may be PREFETCHed/MULTIFETCHed. Widows and orphans may be determined in a single pass. ZZMATCH is a Natural source module which demonstrates a sequential match. A case study: A client designed a month-end report to display account details. Those details were stored in a data base of approximately thirty files. A Natural program was written to read and report the entire master file, drilling down through most of the twenty-nine associated files. The master loop contained five nested READs, each in turn containing one to five nested READs or FINDs, and so on. The program ran very well in test, but by the time the data base contained 100,000 masters and a million records in all, the program was running over thirty hours on a large MVS machine. The program was rewritten as a series of extracts, sorts, and sequential matches. Physical READs and Prefetch were used extensively. Even after the data base had grown to 300,000 masters in a data base of six million records, the report ran consistently in two hours. Top of Tips & Techniques. Most recent tip. Previous tip. Top of this tip. Next tip. Return to Ralph G. Zbrog's home page. Tip 4 07/07/1997 READ syntax In the READ statement I recommend using the BY keyword instead of WITH and the FROM keyword instead of the equal sign ("="). I'll explain by using a real-life example. A programmer coded the following READ MSTR WITH key WHERE/ACCEPT/IF ... for selection criteria FND. FIND TRAN WITH key = MSTR.key DELETE(FND.) After some months in production, transaction volumes grew to the point that the programmer determined that performance would improve by changing the FIND to a READ. READ MSTR WITH key WHERE/ACCEPT/IF ... for selection criteria RD. READ TRAN WITH key = MSTR.key DELETE(RD.) The programmer did not see the error of his ways until the entire transaction file was deleted. I believe that the problem would have been avoided had the programmer applied consistency in his use of READ and FIND syntax. The READ statement should have "looked funny" and caused the code to be questioned. I too have changed FINDs to READs and vice versa, but I always take the time to change the WITH/BY and "="/FROM keywords. READ MSTR BY key WHERE/ACCEPT/IF ... for selection criteria FND. FIND TRAN WITH key = MSTR.key DELETE(FND.) becomes READ MSTR BY key WHERE/ACCEPT/IF ... for selection criteria RD. READ TRAN BY key FROM MSTR.key DELETE(FND.) In the last code segment it is much more obvious that a THRU value is necessary to avoid undesirable results. I think Software AG made an error in equating FROM and "=" in the READ statement's syntax. Top of Tips & Techniques. Most recent tip. Previous tip. Top of this tip. Next tip. Return to Ralph G. Zbrog's home page. Tip 3 07/01/1997 Replacing certain AT BREAKs with a SORT I like the AT BREAK statement. Despite some tricky characteristics, and some quirks which make complicated situations even more difficult to code (I'll leave those for a future Tip), I prefer it to the old 3rd-generation method of saving and comparing key values. It is well worth the effort of learning how to use it. Coding can be as simple as READ file BY STATE-COUNTY-SPDE AT BREAK OF STATE-COUNTY-SPDE WRITE OLD(STATE) OLD(COUNTY) COUNT(TRAN-AMT) SUM(TRAN-AMT) END-BREAK On the other hand the AT BREAK statement isn't one that I use daily; invariably I need to refer to the Reference Manual to refresh my memory on its use. Also DFSORT and SYNCSORT, used by all my clients and most of my previous employers (under MVS; I'm unfamiliar with VSE), are blazingly fast and easier to code in some situations. This tip deals with replacing the COUNT and SUM functions. The example which benefits most from this technique is the one in which no appropriate descriptor exists, so the SORT statement or SORT utility must be used. I'll defer a detailed discussion to a future Tip, but suffice it to say that I always prefer a separate SORT step to an internal sort. This changes the sample code above to READ file PHYSICAL WRITE WORK 1 STATE COUNTY TRAN-AMT SORT step: SORT FIELDS=(1,2,A, 3,3,A), STATE CODE COUNTY CODE FORMAT=CH READ WORK 1 AT BREAK OF #STATE-COUNTY WRITE OLD(STATE) OLD(COUNTY) COUNT(TRAN-AMT) SUM(TRAN-AMT) END-BREAK But since I need to code SORT parameters anyway, I would add SUM FIELDS=(6,5,PD, 11,5,ZD) TRANSACTION AMOUNT COUNT This simplifies the second Natural program to READ WORK 1 WRITE #STATE #COUNTY #COUNT #SUM-AMT END-WORK You can't get much simpler than that! I haven't explained where the COUNT field in position 11 of the WORK record came from. Either add a line containing '00001' to the WRITE WORK statement, or, as I prefer, add an INREC statement to the SORT parameters. INREC FIELDS=(1,10, C'00001') ENTIRE WORK RECORD APPEND A COUNT TO EACH RECORD In my own experience, there have been several situations where an existing extract file contained all the data I needed. Rather than change the Natural extract, which would have required user acceptance testing, I submitted the extract to a sort and used the INREC to append the count field. There are other examples where AT BREAK was still necessary, but coding was simplified by using the SUM and INREC statements of the SORT utility. Top of Tips & Techniques. Most recent tip. Previous tip. Top of this tip. Next tip. Return to Ralph G. Zbrog's home page. Tip 2 06/30/1997 Continuation Symbol The continuation symbol (the hyphen '-') is used to code literals which cannot fit on one Natural source line, as in ....+....1....+....2....+....3....+....5....+....6....+....7.. 1 #A(A100) INIT<'This is a literal which cannot fit on a ' - 'single, 72-character, Natural source line.'> I have four other uses for the continuation symbol. To maintain consistent indentation. To simplify the coding of ad hoc programs which have a starting value in a READ by superdescriptor. To make initialized internal tables easier to read. To impress your friends. Consistent indentation You often see code similar to this ....+....1....+....2....+....3....+....5....+....6....+....7.. IF some-condition THEN REINPUT 'A long message that won"t line up properly' ALARM END-IF but I prefer ....+....1....+....2....+....3....+....5....+....6....+....7.. IF some-condition THEN REINPUT 'A long message that won"t ' - 'line up properly' ALARM END-IF Or going back to the first example ....+....1....+....2....+....3....+....5....+....6....+....7.. 1 #A(A100) INIT<'This is a literal which cannot fit on a ' - 'single, 72-character, Natural source line.'> Starting value in a READ by superdescriptor If you're writing an ad hoc to read a superdescriptor, there is no need for the definitions, redefinitions, and assignments typically associated with the READ of a superdescriptor in a production program. You can specify the starting value within the READ statement itself. ....+....1....+....2....+....3....+....5....+....6....+....7.. 1 #SUPER-DEF 2 #STATE(A2) INIT<'CA'> 2 #COUNTY(P3) INIT<345> 1 REDEFINE #SUPER-DEF 2 #SUPER(A5) ... READ view BY ST-CTY FROM #SUPER becomes ....+....1....+....2....+....3....+....5....+....6....+....7.. READ view BY ST-CTY FROM 'CA' - H'345F' If the superdescriptor contains a D-format field, use a utility like ZZDATES to determine the packed value to be specified in the READ. Initialize internal tables Take the superdescriptor example, above, a step further to initialize an array. ....+....1....+....2....+....3....+....5....+....6....+....7.. 1 #MONTHS 2 #MTH(A3/12) INIT<'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'> 2 #DAYS(N2/12) INIT<30, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31> It's easier to see the relationship of "columns" to "rows" like this: ....+....1....+....2....+....3....+....5....+....6....+....7.. 1 #MONTHS(A5/12) INIT<'Jan' - '30', 'Feb' - '28', 'Mar' - '31', 'Apr' - '30', ... 'Dec' - '31'> 1 REDEFINE #MONTHS 2 #ELEMENT(12) 3 #MTH(A3) 3 #DAYS(N2) For a real-life example of this technique, take a look at the ZZUtils menu program. Impress your friends Is the following Natural code valid? If not, why not? If so, under what conditions, and what, if any, are the results of execution? ....+....1....+....2....+....3....+....5....+....6....+....7.. ASSIGN #A = '456' - '123' ASSIGN #B = 'DEF' - 'ABC' Does #A contain '333'? What about #B? Solution. Top of Tips & Techniques. Most recent tip. Previous tip. Top of this tip. Next tip. Return to Ralph G. Zbrog's home page. Tip 1 06/10/1997 Initialization of a superdescriptor All too often, superdescriptor starting values are initialized at execution time. ....+....1....+....2....+....3....+....5....+....6....+....7.. 1 #SUPER(A4) 1 REDEFINE #SUPER 2 #STATE(A2) 2 #COUNTY(P3) ... ASSIGN #STATE = 'CA' ASSIGN #COUNTY = 345 READ view BY ST-CTY FROM #SUPER I prefer compile-time initialization. ....+....1....+....2....+....3....+....5....+....6....+....7.. 1 #SUPER-DEF 2 #STATE(A2) INIT<'CA'> 2 #COUNTY(P3) INIT<345> 1 REDEFINE #SUPER-DEF 2 #SUPER(A4) ... READ view BY ST-CTY FROM #SUPER Top of Tips & Techniques. Most recent tip. Top of this tip. Next tip. Return to Ralph G. Zbrog's home page. Last updated April 25, 1998, by Ralph G. Zbrog. Solution to Tip 2 quiz. Assuming both #A and #B are of format/length A6, #A contains '456123' and #B contains 'DEFABC'. Return to Tip 2.