Midrange News for the IBM i Community


Scan and Replace Published by: Bob Cozzi on 19 Jul 2011 view comments
© 2011 Robert Cozzi, Jr. All rights reserved.

© Robert Cozzi, Jr. All rights reserved. Reproduction/Redistribution prohibited.
A midrangeNews.com Publication

Merge - Find/Replace Engine

In order to perform a Find and Replace function prior to IBM "i" v7.1 you either had to use something like RPG xTools or roll your own. A number of people used both the %SCAN and %REPLACE built-in functions (can you ever use %REPLACE without looking up its parameters?) and had some level of success.

One of the most powerful features of the RPG xTools FindReplace subprocedure is it's flexible. It includes a parameter that is based on the options in Microsoft WORD's Find/Replace dialog box. You can ignore case, find first only, find next, find all, find whole word only and a few other options. RPG's %SCAN on the other hand, has always be sort of a ridged.

With v7.1, IBM introduced the %SCANRPL (Scan and Replace) built-in function. This built-in function makes it less complex to perform simple find/replace operations. but it is still limited to same-case letters and does not support regular expression syntax. So while %SCANRPL is a welcome addition to RPG IV, it is a very specialized and sadly, case-sensitive find/replace. Here's an example:

   desc = %SCANRPL('/%VENDOR%/' : VNDNAME : ITMDESC);

In this example, the field named ITMDESC (item description) is searched for the pattern: /%VENDOR%/. Where ever instance of that pattern is located and replaced with the value in the field named VNDNAME. The original field's content is not modified, however; instead the modified text string (with the replaced data) is copied to the target field, which is named DESC in this example.

Sponsored by: CNX Corp Valence

You might be asking about the pattern that I choose to use for this example: /%VENDOR%/

This is a standardized substitution pattern that was developed by former IBMer Mel Rothman when he created the CGIDEV2 library. This same pattern was also used later on in RPG xTools' CGILIB and in other 3rd party solutions. It is nothing more than a convention and not something that is built into the operating system or RPG IV.

Uses for Find/Replace

I don't know about you, but I use find/replace all the time. Perhaps its because I have RPG xTools's FindReplace subprocedure available and was aware of it, so I incorporated it into my applications. But others take advantage of FindReplace as well (we have well over 2000 RPG xTools installations). One clever client suggesting using the FindReplace engine that is built into CGILIB as an email mail merge tool.

While RPG xTools customers continue to impress me with their cleverness, this one suggestion caught me off guard. I had never considered using the cgiSetVar subprocedure in CGILIB along with cgiLoadHTML and its other capabilities for anything other than web page generation.

The basic idea is that a template web page (or email template in our customer's example) is created and stored in a source file member or IFS file. Then by embedding the standardized /%VAR%/ substitution pattern values, and utilizing the cgiLoadHTML and cgiSetVar subprocedures in CGILIB, you could easily do an email merge utility. Genius! To send the emails out, just use the Java SendMail tool, or the RPG IV wrapper I created for it years ago, and presented at RPG World. I'll also feature the free SendMail tool in one of next month's issues of RPG Report.

Recently I had another opportunity to use these find/replace capabilities. A client had a set of data that needed to be printed at different locations all over their network. Each printed image needed to have unique data included, this data identified the locations (workstation and printer IDs) where the data was requested and printed. But while the client has already licensed RPG xTools, they did not budget to upgrade to a site-license and install it on all 15 of their outlaying System i boxes.

At first I thought perhaps I could use the new v7.1 %SCANRPL built-in function but I needed more. I needed the capability to not only do a scan and replace, but do it for multiple substitution values over an entire source file member. We settled on embedded the basic printed text "template" inside of a source member to make it easier for their in-house green-screen-PDM/SEU developers to get to the template and modify it using SEU or even RDp.

MergeData Subprocedure

I first set out to create a non-v7.1 implementation of the %SCANRPL built-in function. Why non-v7.1? Because while thier main system is running v7.1, most of the other boxes are at v5r4 with a few at v6r1. So I needed backwards compatibility.

I actually came up with something that is effectively identical to v7.1's %SCANRPL built-in function, I call it MERGEDATA. This subprocedure accepts a search-pattern, replacement-data, and the data to be searched; it then returns an updated text string that included the substitutions. MERGEDATA is a very simple DOW loop that performs a %SCAN followed by a %REPLACE, the source code follows:

     P MergeData       B
     D MergeData       PI           255A   Varying
     D  pattern                     255A   Varying Const
     D  newData                     255A   Varying Const
     D  inData                      255A   Varying Const

     D  mergeData      S            255A   Varying
     D  pos            S             10I 0 
      /free 
           merged = inData;
           pos = %scan(pattern : merged);
           dow (pos > 0);
              merged = %Replace(newData : merged : Pos : %Len(pattern)); 
              pos = %scan(pattern : merged);
           enddo;
           return merged;
      /end-free
     P MergeData       E

I've limited this implementation to a 255-character input length simply because source member tend to be 112 bytes or less so 255 seems "good enough". If you need a larger input/output length, there's nothing stopping you from increasing the parameter's length. Just be aware that if your input data causes the merged result to exceed the 255-bytes of the return variable, you will have truncation. So consider increasing the return value's length if necessary.

NOTE: The RTNPARM keyword is NOT supported in RPG IV until v7.1. This keyword helps the performance of lengthy text strings returned from subprocedures (such as that used in MERGEDATA). However in v7.1 you can simply use %SCANRPL instead of MERGEDATA so there's not much advantage to including RTNPARM here because (a) this subprocedure is intended for use on v6.1 and earlier, and (b) if you are on v7.1 you can use %SCANRPL instead of MergeData.

To implement this routine along with the ability to search and replace data inside a source member, you could create a File spec in the RPG IV source code that allows the caller to specify the source file and member names. To do this, use the EXTFILE and EXTMBR keywords, as follows:

     FQTXTSRC   IF   E             DISK    EXTFILE(srcFile) EXTMBR(srcMbr)  
     F                                     USROPN RENAME(QTXTSRC:TEXTREC)

I use QTXTSRC as the source file name. Since it is Externally Described, it must exist when the code is compiled. You could change this to any source file that lives on your system, however.

When the QTXTSRC source file is opened, specify the names of the actual source file and member names to be opened by inserting them into the SRCFILE and SRCMBR fields. Those fields are defined, with default initial values as follows:

     D srcFile         S             21A   Inz('*LIBL/QTXTSRC')
     D srcMbr          S             10A   Inz('*FIRST')

Typically you would pass these names as parameters into the program or subprocedure doing the scan/replace task. The source member can be a valid member name or one of the 3 special values:

  • *ALL - Opens all members and processes them as if they were one, large member.
  • *FIRST - Opens the first member added to the file.
  • *LAST - Opens the last member added to the file (undocumented)

NOTE: When using *LAST, you can specify EXTMBR('*LAST') on the File spec, but you can specify it within a variable (as we are doing in this example) and then open the file--it works.

Once these fields contain a valid source file and member name, you can open the file using the RPG IV OPEN operation code. Immediately after the OPEN, check the %ERROR() built-in function. If an error occurred, chances are the OPEN failed because the file or member doesn't exist. To use %ERROR, you MUST specify the "E" operation extend on the OPEN opcode, i.e., OPEN(e)

Here's the an example:

 ..... /free
01        open(e) QTxtSrc;                              
02         if NOT %error();                               
03            read QTxtSrc;                              
04            dow NOT %EOF();                             
05               if (%len(%trim(srcDta)) >= 2);           
06                  srcData = %Trim(srcDTA);              
07                  if (%subst(srcData:1:2)<>'//');       
08                     if ((%subst(srcData:1:2)='/END') or
09                         (%subst(srcData:1:2)='/end')); 
10                       LEAVE;                         
11                     endif;                             
12                     srcData = MergeData('/%WSID%/' : WSID : srcData);
13                     srcData = MergeData('/%PRTID%/' : PRTID : srcData);
14                     srcData = MergeData('/%VENDOR%/' : vndName : srcData);
15                     srcData = MergeData('/%CUSTNO%/' : %char(CSTNBR) : srcData);
16                     srcData = MergeData('/%DATETIME%/ : %char(%timeStamp()) : srcData)
17                     CALLP  PrintOnTheLocalPrinter( srcData );  // This is your print routine.
18                  endif;                                
19               endif;                                   
20               read QTxtSrc;                           
21            enddo;                                      
22         endif;                                         
23         close(e) QTxtSrc;                             
24         return;                                        	
..... /end-free

Lines 1 through 4 open the identified source file member and begin the READ loop.

Lines 5 through 11 test the first few characters of each line of the source file member. This allows you to embed comments in the source member itself, those lines are ignored and not included in the output/result.

Lines 12 through 16 call the MergeData subprocedure and perform a scan/replace over the data. The results are returned and copied into the SRCDATA field.

The SRCDATA field is printed inside the PrintOnTheLocalPrinter subprocedure. Obviously I fabricated this name to identify what it is supposed to be doing.

Example Merge Data

     // This text is merged with the vendor master file.
     // The workstation and printer ID are embedded along with
     // The vendor name and customer number.
     //  Note the date/time stamp is included for logging purposes.
STATION: /%WSID%/ - /%PRTID%/  - /%DATETIME%/
CUST: /%CUSTNO%/
VENDOR: /%VENDOR%/

This is an example of a source member that is read into our program and merged with real data. The final 3 lines contain substitution patterns that are replaced with real data.The first 4 lines in this source member are comments. I've set up the example program to ignore (skip) source member lines that begin with two forward slashes (the new style RPG IV comments). In addition if the first item on any source line is /END then the routine ends. This forces an end-of-file (EOF) condition without actually reading to end-of-file. I included this feature because I often debug the output by omitting portions of the source member. But rather than physically deleting the lines, I simply include the /END at whatever location I want to terminate the output.

 

FreshBooks

Other Merge's

The purpose of this routine was to provide a way to print dynamic output to any of several printers on our network. But it can also be used to create custom emails from a mailing list and even customized screens--just output the text to the display rather than a printer. Remember, the substitution variables are your own creation--you can use any names or syntax you prefer--I simply chose the convention used in CGIDEV2 and RPG xTools. Of course this example only supports case-sensitive variables, so you should take that into consideration when using this example code.

As I mentioned, to send out emails, use the RPG version of the Java SendMail tool. I published this at one of the popular RPG World Conferences a few years ago and continue to use it to this day--in fact I use the very technique featured in this newsletter to send out this newsletter. I'll try to feature the SendMail utility in one of next month's issues of RPG Report.

In the meantime, if you come up with any other cool use or idea for this kind of file-level scan/replace function, I'd love to hear about it. Of course if you need more sophisticated capabilities, check out the FindReplace subprocedure in RPG xTools it is very well suited for web page generation. And if you are already running IBM "i" v7.1, take a look at the %SCANRPL built-in function, it is not perfect, but it is very cool itself.

You're Welcome!

Call Me

Bob Cozzi is available for consulting or on-site training in RPG IV, SQL, and CGI/Web as well as perform contract programming. Currently many shops are asking Cozzi to join them for 1 to 3 days of Q&A and consulting with their RPG staff. The Staff gets to ask real-world questions that apply to their unique situations. Contact Cozzi by sending him an email at: bob at rpgworld.com

Look for Bob in the upcoming STARZ network TV production of "BOSS" a TV series staring Kelsey Grammer of "Frasier" and "Cheers". Coming this fall only on the STARZ network.

Bob also accepts your questions for use in future RPG Report articles or content for midrangeNews.com. Topics of interest include: RPG IV, Web development with RPG IV, APIs, C/C++ or anything else IBM i development related (except subfiles, data areas and RPGII/III because Bob doesn't care about that stuff) write your questions using the Feedback link on the midrangeNews.com website. 

You can subscribe to RPG Report (we call it "follow") by visiting the RPG Report page on midrangeNews.com and then click the FOLLOW link in the table of contents for that page. To unsubscribe, simply click that same link. You must be signed up and signed in to midrangeNews.com to start following RPG Report.

Follow Bob Cozzi on Twitter

Return to midrangenews.com home page.
Sort Ascend | Descend

COMMENTS