Easily find text strings, regardless of case.
In the last article, "Reading a Message File," we used the Retrieve Message API QMHRTVM to sequentially read the messages of a message file and display them using a subfile. In this article, we'll implement a find capability that will allow us to quickly find all messages containing one or more character strings (words).
As a character string such as "library" may be stored in a message description as "Library," "library," or even "LIBRARY" and a good find function should find all instances, we'll also monocase the message description text prior to scanning it for the character strings of interest. To perform this monocasing, we'll use the Convert Case API documented here. The Convert Case API is available as both a program (QLGCNVCS) and as a procedure export (QlgConvertCase) from a service program. In the following sample program, we'll use the QlgConvertCase procedure.
The previous article created the command FNDMSGS, which defined the optional parameter MSGF (message file). The MSGF keyword was used to specify the qualified message file that was to be used, with a default of *LIBL/TEXTMSGS. Today, we'll add the required parameter STRING. The STRING keyword allows you to specify from one to three character strings that must be found in the first-level text of any message in the message file in order for the message to be displayed using the FNDMSGS command.
To add prompt text for the STRING parameter, run the following Add Message Description command.
ADDMSGD MSGID(CMD0004) MSGF(TEXTMSGS) MSG('Find string')
The message description CMD0004 will be added to the TEXTMSGS message file with the first-level text of 'Find string'.
To add the new STRING parameter to the FNDMSGS command, add a new PARM command to the command source used last month. The STRING parameter should be defined before the MSGF parameter as it is a required parameter. Below is the entire command definition for FNDMSGS with the STRING parameter being defined with the first PARM command shown.
CMD PROMPT(CMD0001)
PARM KWD(STRING) TYPE(*CHAR) LEN(20) MIN(1) +
MAX(3) PROMPT(CMD0004)
PARM KWD(MSGF) TYPE(QUALNAME) PROMPT(CMD0002)
QUALNAME: QUAL TYPE(*NAME) LEN(10) DFT(TEXTMSGS)
QUAL TYPE(*NAME) DFT(*LIBL) SPCVAL((*LIBL) +
(*CURLIB)) PROMPT(CMD0003)
STRING is a required parameter as specified by MIN(1), and up to three character strings can be entered as indicated by MAX(3). Each character string can be up to 20 bytes in length due to the specification of LEN(20), and the text for the STRING parameter, when prompting the FNDMSGS command, comes from message description CMD0004.
To create the updated FNDMSGS command, run the following Create Command command.
CRTCMD CMD(FNDMSGS) PGM(FNDMSGS) ALLOW(*INTERACT *IPGM) PMTFILE(TEXTMSGS)
The updated FNDMSGS command processing program (CPP) source is shown below. As a reading aid, the changed portions of the program are shown in bold.
h dftactgrp(*no) bnddir('MSGSPTBD')
fFndMsgsDF cf e workstn sfile(Msg_Sfl :MsgSflRRN1)
dFndMsgs pr
d NbrOfStrings 5i 0
d QualMsgF_in 20a const
dFndMsgs pi
d NbrOfStrings 5i 0
d QualMsgF_in 20a const
dGetNewMsgF pr
dGetNxtMsgs pr
d NbrMsgsToGet 5u 0 const
dGetCurMsg pr
d MsgID 7a const
dRtvMsgTxt pr
dMsgRcv 65535a options(*varsize)
dLenMsgRcv 10i 0 const
dMsgID 7a const
dRunCmd pr n
d Cmd 512a const
dSndEscape pr
dSetUp pr
dRtvMsgAPI pr extpgm('QMHRTVM')
d MsgInfo 65535a options(*varsize)
d LenMsgInfo 10i 0 const
d Format 8a const
d MsgID 7a const
d QualMsgF 20a const
d RplDta 65535a const
d LenRplDta 10i 0 const
d RplOpt 10a const
d RtnFmtOpt 10a const
d ErrCde likeds(QUSEC)
d RtvOpt 10a const options(*nopass)
d ToCCSID 10i 0 const options(*nopass)
d RplDtaCCSID 10i 0 const options(*nopass)
dSndMsg pr extpgm('QSYS/QMHSNDPM')
d MsgID 7 const
d QualMsgF 20 const
d MsgDta 65535 const options(*varsize)
d LenMsgDta 10i 0 const
d MsgType 10 const
d CallStackEntry 65535 const options(*varsize)
d CallStackCntr 10i 0 const
d MsgKey 4
d QUSEC likeds(QUSEC)
d LenCSE 10i 0 const options(*nopass)
d CSEQual 20 const options(*nopass)
d DSPWaitTime 10i 0 const options(*nopass)
d CSEType 10 const options(*nopass)
d CCSID 10i 0 const options(*nopass)
dMonoCase pr extproc('QlgConvertCase')
d CtlBlk 65535a const options(*varsize)
d SourceString 65535a const options(*varsize)
d OutputString 65535a options(*varsize)
d LenSrcString 10i 0 const
d QUSEC likeds(QUSEC)
dProcessCmd pr extpgm('QCAPCMD')
d SourceCmd 65535a const options(*varsize)
d LenSrcCmd 10i 0 const
d CtlBlk 65535a const options(*varsize)
d LenCtlBlk 10i 0 const
d Format 8a const
d ChgCmd 1a options(*varsize)
d LenAvlChgCmd 10i 0 const
d LenRtnChgCmd 10i 0
d QUSEC likeds(QUSEC)
dRtvObjD pr extpgm('QUSROBJD')
d ObjInfo 65535a options(*varsize)
d LenObjInfo 10i 0 const
d Format 8a const
d QualObjN 20a const
d ObjType 10a const
d QUSEC likeds(QUSEC) options(*nopass)
d ASPCtl 65535a const options(*nopass)
/copy qsysinc/qrpglesrc,qlg
/copy qsysinc/qrpglesrc,qmhrtvm
/copy qsysinc/qrpglesrc,qcapcmd
/copy qsysinc/qrpglesrc,qusrobjd
/copy qsysinc/qrpglesrc,qusec
dRcvVar ds qualified
d API likeds(QMHM030000)
d MsgDta 4096a
dObjInfo ds qualified
d API likeds(QUSD0100)
dQualMsgF ds
d Msg_File 10
d Msg_Lib 10
dErrCde ds qualified
d Common likeds(QUSEC)
d MsgTxt 512
dStringPtr s *
dString s 20 based(StringPtr)
dMsgDtaUpr s 4096
dFound s n
dMsgSflRRN1 s 4 0
dTopSflRRN1 s 4 0
dSizeOfSFLPage s 5u 0 inz(12)
dTopMsgID s 7a
dRun s n
dUserAction s n
dPrvMsg_File s like(Msg_File)
dPrvMsg_Lib s like(Msg_Lib)
dNotUsedChr s 1
dNotUsedInt s 10i 0
/free
SetUp();
dow (not *in03);
select;
when ((Msg_File <> PrvMsg_File) or
(Msg_Lib <> PrvMsg_Lib));
GetNewMsgF();
when *in05;
GetNewMsgF();
when *in41;
GetNxtMsgs(SizeOfSFLPage);
when TopSflRRN1 > 0;
UserAction = *off;
readc Msg_Sfl;
dow (not %eof(FndMsgsDF));
select;
when Option = '2';
GetCurMsg(MsgID);
Run = RunCmd('?CHGMSGD ?*MSGID(' + MsgID +
') ?*MSGF(' + %trimr(Msg_Lib) +
'/' + %trimr(Msg_File) +
') MSG(''' +
%subst(RcvVar
:(RcvVar.API.QMHOMRtn + 1)
:RcvVar.API.QMHLMRtn04) +
''')');
if Run;
GetCurMsg(MsgID);
MsgTxt = %subst(RcvVar
:(RcvVar.API.QMHOMRtn + 1)
:RcvVar.API.QMHLMRtn04);
endif;
UserAction = *on;
when Option = '4';
Run = RunCmd('RMVMSGD MSGID(' + MsgID +
') MSGF(' + %trimr(Msg_Lib) +
'/' + %trimr(Msg_File) + ')');
if Run;
RtvMsgTxt(MsgTxt :%size(MsgTxt)
:'DSP5001');
endif;
UserAction = *on;
when Option = '5';
Run = RunCmd('DSPMSGD RANGE(' + MsgID +
') MSGF(' + %trimr(Msg_Lib) +
'/' + %trimr(Msg_File) + ')');
UserAction = *on;
endsl;
if *in03;
leave;
endif;
Option = *blanks;
update Msg_Sfl;
readc Msg_Sfl;
enddo;
if (not UserAction);
leave;
endif;
other;
// Nothing left to do
endsl;
write Msg_Keys;
*in31 = (TopSflRRN1 > 0);
exfmt Msg_Ctl;
enddo;
*inlr = *on;
return;
/end-free
pGetNxtMsgs b
dGetNxtMsgs pi
d NbrMsgsToGet 5u 0 const
dmyX s 5u 0
dmyY s 5u 0
/free
if TopMsgID <> RcvVar.API.QMHMID;
// Need to refresh the first message to retrieve
GetCurMsg(TopMsgID);
endif;
*in40 = *on;
MsgSflRRN1 = TopSflRRN1;
for myX = 1 to (NbrMsgsToGet + 1);
MonoCase(QLGIDRCB00
:%subst(RcvVar :(RcvVar.API.QMHOMRtn + 1)
:RcvVar.API.QMHLMRtn04)
:MsgDtaUpr :RcvVar.API.QMHLMRTN04 :QUSEC);
Found = *on;
for myY = 1 to NbrOfStrings;
StringPtr = %addr(NbrOfStrings) + %size(NbrOfStrings) +
((myY - 1) * %size(String));
if (%scan(%trimr(String)
:%subst(MsgDtaUpr :1 :RcvVar.API.QMHLMRTN04))) = 0;
Found = *off;
leave;
endif;
endfor;
select;
when not Found;
myX -= 1;
when myX > NbrMsgsToGet;
leave;
other;
MsgID = RcvVar.API.QMHMID;
MsgTxt = %subst(RcvVar :(RcvVar.API.QMHOMRtn + 1)
:RcvVar.API.QMHLMRtn04);
MsgSflRRN1 += 1;
write Msg_Sfl;
*in40 = *off;
endsl;
RtvMsgAPI(RcvVar :%size(RcvVar) :'RTVM0300'
:RcvVar.API.QMHMID :QualMsgF
:' ' :0 :'*NO' :'*NO' :QUSEC
:'*NEXT' :0 :0);
if RcvVar.API.QMHBAVL09 = 0;
// End of message descriptions if bytes avail = 0
*in35 = *on;
leave;
else;
TopMsgID = RcvVar.API.QMHMID;
endif;
endfor;
TopSflRRN1 = MsgSflRRN1;
Sfl_DspPag = MsgSflRRN1;
/end-free
pGetNxtMsgs e
*
pGetNewMsgF b
dGetNewMsgF pi
/free
*in32 = *on;
write Msg_Ctl;
*in32 = *off;
MsgSflRRN1 = 0;
TopSflRRN1 = 0;
*in35 = *off;
RtvMsgAPI(RcvVar :%size(RcvVar) :'RTVM0300'
:' ' :QualMsgF
:' ' :0 :'*NO' :'*NO' :ErrCde
:'*FIRST' :0 :0);
select;
when ErrCde.Common.QUSBAvl > 0;
SndEscape();
when RcvVar.API.QMHBAVL09 = 0;
// No messages in message file
*in35 = *on;
other;
TopMsgID = RcvVar.API.QMHMID;
GetNxtMsgs(SizeOfSFLPage);
endsl;
if %subst(Msg_Lib :1 :1) = '*';
// Resolve special values like *LIBL and *CURLIB
RtvObjD(ObjInfo :%size(ObjInfo) :'OBJD0100'
:QualMsgF :'*MSGF' :QUSEC);
Msg_Lib = ObjInfo.API.QUSRL01;
endif;
PrvMsg_File = Msg_File;
PrvMsg_Lib = Msg_Lib;
/end-free
pGetNewMsgF e
*
pGetCurMsg b
dGetCurMsg pi
d MsgID 7a const
/free
RtvMsgAPI(RcvVar :%size(RcvVar) :'RTVM0300'
:MsgID :QualMsgF
:' ' :0 :'*NO' :'*NO' :QUSEC
:'*MSGID' :0 :0);
/end-free
pGetCurMsg e
*
pRunCmd b
dRunCmd pi n
d Cmd 512a const
/free
ProcessCmd(Cmd :%len(%trimr(Cmd))
:QCAP0100 :%size(QCAP0100) :'CPOP0100'
:NotUsedChr :0 :NotUsedInt :ErrCde);
if ErrCde.Common.QUSBAvl > 0;
select;
when ErrCde.Common.QUSEI = 'CPF6801';
if %subst(ErrCde.MsgTxt :1 :3) = 'F3 ';
*in03 = *on;
return *off;
else;
return *off;
endif;
other;
SndEscape();
endsl;
else;
return *on;
endif;
/end-free
pRunCmd e
*
pSetUp b
dSetUp pi
/free
QUSBPrv = 0;
ErrCde.Common.QUSBPrv = %size(ErrCde);
// Process command API control structure
QCAP0100 = *loval; // init API ctls to nulls
QCACMDPT = 0; // Run command
QCABCSDH = '0'; // Ignore DBCS
QCAPA = '2'; // Prompt if controls found
QCACMDSS = '0'; // Use i OS syntax
// Convert case API control structure
QLGIDRCB00 = *loval; // init API ctls to nulls
QLGTOR02 = 1; // CCSID based casing
QLGIDOID00 = 0; // Use job CCSID
QLGCR00 = 0; // Convert to uppercase
QualMsgF = QualMsgF_in;
GetNewMsgF();
write Msg_Keys;
*in31 = (TopSflRRN1 > 0);
exfmt Msg_Ctl;
/end-free
pSetUp e
*
pSndEscape b
dSndEscape pi
dLenMsgDta s 10i 0
dMsgKey s 4a
/free
if ErrCde.Common.QUSBAVL < 16;
LenMsgDta = 0;
else;
LenMsgDta = ErrCde.Common.QUSBAVL - %size(ErrCde.Common);
endif;
SndMsg(ErrCde.Common.QUSEI :'QCPFMSG *LIBL'
:ErrCde.MsgTxt :LenMsgDta
:'*ESCAPE' :'*PGMBDY' :1 :MsgKey :QUSEC);
/end-free
pSndEscape e
As we have added a new parameter to the FNDMSGS command definition, we need to change the prototype and procedure interface of the CPP. In the sample program, this is done by adding the NbrOfStrings parameter definition to the FNDMSGS pr and pi specifications.
The STRING parameter of the FNDMSGS command is defined as a simple list—a list of from one to three values where each value is of the same type. This type of list, when passed to a CPP written in a high-level language such as RPG, is formatted as a 2-byte binary value representing the number of values passed to the program, followed immediately by the specified number of passed parameter values. In the case of the STRING character value inputs, each value will be left-adjusted and blank-padded to a length of 20 bytes.
The STRING parameter can be defined several ways within the RPG application program. One common approach is to define the parameter as a data structure containing a 2-byte binary value followed by a 60-byte character string (a maximum of three values, each 20 bytes in size). The program can then access the string values using substring operations against the 60-byte subfield within the bounds of the 2-byte binary value representing the number of STRING values passed to the program. A second approach is to define the parameter as a data structure containing a 2-byte binary value followed by an array of three 20-byte elements. The program can then access the string values using array indexing within the bounds of the 2-byte binary value representing the number of STRING values passed to the program. A third approach, and the one taken in the sample program, is to define only the 2-byte binary value representing the number of STRING values passed to the program. The FNDMSGS program, as you will see shortly, then accesses the STRING values using a pointer and a based variable.
Using a pointer approach eliminates the need for the RPG program to explicitly code a character string of 60 bytes, as in the first approach, or an array of three elements, as in the second solution. If users of the FNDMSGS command were to request an enhancement next year to allow up to five search values, you would only need to change the command definition if the search values are accessed using a pointer approach. With the other solutions, you would also need to change the RPG program, making the character string 100 bytes in length when using the substring approach, or changing the DIM(3) declaration to DIM(5) when using the array approach.
The next two changes to the RPG program are related to the use of the Convert Case API. This API is used to uppercase the first-level text of retrieved messages so that the program can easily find all messages containing the text specified by the FNDMSGS STRING parameter (the i operating system command analyzer will by default uppercase the STRING values if you do not quote the searched-for values, which the FNDMSGS program is implicitly assuming you will not do). First, the program provides a prototype, MonoCase, defined as a procedure call to the QlgConvertCase API with five parameters. The first parameter is a control block passed to the API and specifies the type of conversion to be performed. The second parameter of the Convert Case API is the textual data to be monocased, the third parameter is used to receive the monocased data from the API, and the fourth parameter specifies the length of the textual data to be monocased. The last parameter is the standard API error code data structure. Having prototyped the Convert Case API, the program also uses the /copy directive to include the QLG source member of QSYSINC/QRPGLESRC. This member provides a data structure, QLGIDRCB00, which defines the Convert Case control block parameter. The FNDMSGS program initializes this structure in the SetUp procedure.
Related to the search for messages containing the user-specified STRING values, several variables are also declared. StringPtr and String are used to access the STRING values, MsgDtaUpr to hold the uppercased first-level text of retrieved messages (a separate variable is used for the uppercased message text so that the subfile will continue to display the message text in mixed case), and Found is an indicator to represent whether or not all STRING values were found in the retrieved first-level text of a message.
The functional changes to the program can be found in the GetNxtMsgs procedure. As with last month's version of the GetNxtMsgs procedure, the intent of the routine is to load the next subfile page of messages and determine whether or not the end of the subfile messages has been reached. The actual processing has changed quite a bit, though, as the program is now conditionally writing messages to the subfile based on the message text containing all of the specified STRING values.
As determining whether or not to set SFLEND is dependent on there being another message containing all of the STRING values (as opposed to just any "next" message existing), the main FOR loop of GetNxtMsgs is changed from running NbrMsgsToGet to (NbrMsgToGet + 1).
Upon entering the FOR group, the message currently being processed is converted to uppercase using the Convert Case API (named MonoCase within the program). The first parameter passed to the API is the Request control block structure QLGIDRCB00. The Convert Case API provides three ways to monocase a text string with the Request control block structure controlling which method is to be used. One option, and the one used by the sample program, is to specify the CCSID encoding of the textual data. The other options are to name a table object to use or to provide a user-defined conversion table directly. When a CCSID-based conversion is specified, you indicate what CCSID to use and whether to uppercase or lowercase the data. In the SetUp procedure, the Request control block is formatted for a CCSID-based conversion (QLGTOR02 is set to 1) to uppercase (QLGCR00 is set to 0) using the job CCSID (QLGIDOI00 is set to 0). The Retrieve Message API, due to our taking the default for the CCSID to convert to parameter, is converting the retrieved message text to the job CCSID.
While the SetUp procedure has only four statements related to properly initializing the QLGIDRCB00 control block (not counting the comment) for uppercasing by job CCSID, two of the four are technically not needed. Both the CCSID to use (QLGIDOID00) and the conversion request (QLGCR00) subfields of QLGIDRCB00 are defined as integer values, so when SetUp set QLGIDRCB00 to *loval, SetUp also implicitly set these two variables to 0. The redundant setting of QLGIDOID00 to 0 (use job CCSID) and of QLGCR00 to 0 (uppercase) is done in SetUp solely as an aid in seeing what the program is doing.
The remaining three parameters passed to the Convert Case API are the text to be uppercased, the variable to receive the uppercased data, and the length of the data to uppercase, respectively. The substring operation used to identify the text to uppercase is the same as last month's substring operation to move the message text to the subfile field MsgTxt. In case you're wondering, the variables used for the second and third parameters (the input and output parameters of the API) can identify the same variable, so FNDMSGS could uppercase the message text in place. As mentioned earlier in this article, though, a separate output variable is used as I prefer to show the first-level message text in mixed case (and, as written, FNDMSGS doesn't load the first-level message text until after Convert Case uppercases the text and the STRING values are found in the message text).
At this point (if not earlier…), some readers may be wondering why the FNDMSGS program is using the Convert Case API rather than RPG's translate (XLATE or %xlate) support. The translate function can certainly be used for the monocasing of textual data, but it has a limitation that may not be immediately obvious—namely, that the characters specified in the From and To strings passed to xlate are stored in the CCSID of the module, and the module source will only be in one CCSID. If you are confident that all textual data your programs need to monocase will be in only one CCSID and you can clearly identify the characters that need to be monocased (for instance, many lowercase to uppercase xlate-based From and To strings seem to forget about names containing â, ä, à, á, ã, and ä, just to mention a few variants of "a" that are all valid in CCSID 37), then the translate function may be sufficient for your needs. As using the Convert Case API is quite simple, and the API provides support if you ever do need to work with multiple CCSIDs or an expanded set of characters, I recommend using the API across the board for textual monocasing. Having uppercased the message text, the program sets indicator Found to *on (providing a default that the message does contain all user-specified STRING values) and then searches the current message for each specified value.
A FOR group, conditioned by the number of values to search for, is used to scan the message text. As soon as any searched-for value is not found, indicator Found is set to *off and the FOR loop exited. You might note that this Found indicator is not really necessary. You could remove all references to the Found indicator and still accomplish this search/validation function. This indicator is included solely for ease of reading. The test for 'not Found', located later in the program, can be accomplished simply by checking if myY (the control variable of the FOR loop) is less than or equal to NbrOfStrings. The only time myY will be greater than NbrOfStrings is when the FOR loop was successfully run for all STRING values (that is, indicator Found would have still been "on").
As mentioned earlier in this article, a based variable is used to identify the STRING values to be searched for. The based variable is String, and the basing pointer is StringPtr. The location of each String value is calculated by taking the address of the NbrOfStrings parameter (the 2-byte binary value passed as a parameter to the FNDMSGS program), adding the size of the NbrOfStrings field (2 bytes), and then adding the offset from this location to the string value to be used. The first passed STRING value is found immediately after the NbrOfStrings field (that is, an offset of 0), the second STRING value 20 bytes after the first value (or an offset of 20), and so on. To calculate the appropriate offset, the program takes the FOR control value (variable myY), subtracts 1, and then multiplies this value by the size of each string (20 bytes). StringPtr is then pointing to the correct user-specified String value.
The program now uses the %scan built-in to scan for the String value within the uppercased message text (MsgDtaUpr) for the length of the message text. If %scan returns 0, the String value was not found, indicator Found is set to *off, and the FOR group is left. Otherwise, the FOR group is re-run using the next user-specified String value.
Upon exiting the FOR group, either due to a String value not being found or all String values being found, a SELECT is entered. If a String value was not found, the outer FOR loop control value (myX) is decremented as the message is not being written to the subfile, the next message from the message file is retrieved, and the outer FOR group re-run. If the outer FOR loop control value (myX) indicates that the subfile page is full and that the next message to be written to the subfile has been found (myX > NbrMsgsToGet), then the outer FOR group is left and the subfile page containing the most recent set of messages will be shown. Otherwise, a message has been found that contains all of the user-specified String values, the message is written to the subfile, the next message from the message file is retrieved, and the outer FOR group re-run. The actual writing of the message to the subfile is the same as was done last month.
You can compile the FNDMSGS program using this command:
CRTBNDRPG PGM(FNDMSGS)
Run the program using the FNDMSGS command. Prompt FNDMSGS, and search for strings such as "library" in message file QCPFMSG. As that returns quite a few messages, try the command again using string values of "library," "file," and "not." Still quite a few matches, so change the MAX(3) of the FNDMSGS command PARM definition for STRING to MAX(5), re-create the FNDMSGS command using the CRTCMD command provided at the start of this article (note that you do not need to recompile the FNDMSGS program), and try "library," "file," "not," and "authorized." As you can see, the FNDMSGS program works as-is with the larger number of user-specified values.
Hopefully, you will find FNDMSGS to be a useful utility and will add the Convert Case API to your developer's toolkit. Convert Case is an API that can be utilized when you need to uppercase or lowercase text in order to simplify sorting or searching. For FNDMSGS, it's useful when searching message text, but it can be applied just as easily to customer names and other textual information. Next month, we'll look at additional APIs that are available to you.
In the meantime, if you have any API questions, send them to me at
as/400, os/400, iseries, system i, i5/os, ibm i, power systems, 6.1, 7.1, V7, V6R1
LATEST COMMENTS
MC Press Online