Find characters within a variable.
Last month, in "A CL-Based Implementation of RPG Built-in %Check," we built the command Check Characters (CHKCHR). The CHKCHR command returns the position of the first character within a CL variable that is not found within a list of valid values. Based on reader requests, this month we'll look at how to implement a Scan Characters (SCNCHR) command. The SCNCHR command will return the position of the first character within a CL variable that is found in a list of valid values.
Note that SCNCHR is not the same as the RPG built-in %Scan. If you were to specify %scan('abc' :'a cabbage') in an RPG program, the %Scan built-in would return a value of 0 as the string 'abc' is not found within the string 'a cabbage'. The SCNCHR command on the other hand, which is patterned after last month's CHKCHR command, would return a value of 1. This is because SCNCHR will be looking for the first occurrence of an 'a', a 'b', or a 'c' within the string 'a cabbage'. In a future article, we will look at how to build a CL command (perhaps named Scan for String (SCNSTR)) that will more closely mimic the behavior of RPG's %Scan built-in.
The SCNCHR Command
As we did last month, we'll start by providing a command definition for the SCNCHR command:
Cmd Prompt('Scan for Characters')
Parm Kwd(Chars) Type(*Char) Len(192) Min(1) +
Prompt('Characters to search for')
Parm Kwd(Base) Type(*Char) Len(1024) Min(1) +
Prompt('String to be scanned')
Parm Kwd(Pos) Type(*UInt4) Min(1) +
Prompt('Position of first match')
Parm Kwd(StrPos) Type(*UInt4) Dft(1) +
Rel(*Gt 0) +
Prompt('Starting position within Base')
The first three parameters are defined in a manner very similar to what was used for last month's CHKCHR command and will not be further discussed. The fourth parameter however, Starting position (or StrPos), is new. This parameter allows you to specify the starting position, within the Base variable, for where the scan should start. The default value for the StrPos keyword is 1 (start at the initial character of the string identified by the Base keyword), and the value specified must be greater than 0. You will soon see that another check, to verify that the starting position is not greater than the length of the Base string, is done within the command processing program (CPP) of the SCNCHR command.
Assuming that the previous command source is stored in member SCNCHR of source file QCMDSRC, you can create SCNCHR with the following command:
CrtCmd Cmd(ScnChr) Pgm(ScnChrCPP) Allow(*BPgm *IPgm)
This command indicates that the CPP of SCNCHR is program SCNCHRCPP and that the command can be run from within a program. As with CHKCHR, you will not be able to run the SCNCHR command interactively from a command line due to the Pos parameter being a return value.
Prior to looking at the source of our CPP, the ScnChrCPP program is going to be using a new command—Retrieve Variable Length (RtvVarLen)—which will return the blank trimmed length of the Base string passed to the ScnChr command. Following is the command and CL source for the RTVVARLEN command.
A RTVVARLEN Command
Cmd Prompt('Retrieve Variable Length')
Parm Kwd(Var) Type(*Char) Len(5000) Min(1) +
Vary(*Yes *Int4) Prompt('Variable name')
Parm Kwd(Length) Type(*UInt4) RtnVal(*Yes) +
Min(1) Prompt('Variable length')
Assuming the command source is stored in member RTVVARLEN of source file QCMDSRC, you can create the command using this command:
CrtCmd Cmd(RtvVarLen) Pgm(RtvVarLen) Allow(*BPgm *IPgm)
A RTVVARLEN Command Processing Program
Pgm Parm(&Var &Length)
Dcl Var(&Var) Type(*Char) Len(5000)
Dcl Var(&VarLength) Type(*UInt) Stg(*Defined) +
Dcl Var(&Length) Type(*UInt)
ChgVar Var(&Length) Value(&VarLength)
Assuming the preceding CL source is stored in member RTVVARLEN of source file QCLSRC, you can create the RTVVARLEN CPP using this command:
The ability to build a command such as RTVVARLEN was alluded to in the article "Just How Big Is That Variable?" back in October of 2008. For a description of how to access both character variable size and length information, refer to that earlier article. Note that we could have simply defined the Base parameter of the SCNCHR command as Vary(*Yes *Int4) and directly accessed the blank trimmed length of &Base, but I thought providing a general-purpose command such as RTVVARLEN might be more useful to some readers.
With the RTVVARLEN command behind us, below is the source for our CPP SCNCHRCPP.
The SCNCHRCPP Command Processing Program
Pgm Parm(&Chars &Base &Pos &StrPos)
Dcl Var(&Chars) Type(*Char) Len(192)
Dcl Var(&Base) Type(*Char) Len(1024)
Dcl Var(&Pos) Type(*UInt)
Dcl Var(&StrPos) Type(*UInt)
Dcl Var(&Null_Chars) Type(*Char) Len(193)
Dcl Var(&Null_Base) Type(*Char) Len(1025)
Dcl Var(&Null_Char) Type(*Char) Len(1) Value(x'00')
Dcl Var(&DataToMove) Type(*UInt)
Dcl Var(&MsgDta) Type(*Char) Len(8)
Dcl Var(&MsgStrPos) Type(*UInt) +
Stg(*Defined) DefVar(&MsgDta 1)
Dcl Var(&Len) Type(*UInt) +
Stg(*Defined) DefVar(&MsgDta 5)
RtvVarLen Var(&Base) Length(&Len)
If Cond(&StrPos > &Len) Then(Do)
ChgVar Var(&MsgStrPos) Value(&StrPos)
SndPgmMsg MsgID(ESC0003) MsgF(OurMsgs) +
ChgVar Var(&Null_Chars) Value(&Chars *TCat &Null_Char)
ChgVar Var(&DataToMove) Value(&Len - &StrPos + 1)
ChgVar Var(&Null_Base) +
Value(%sst(&Base &StrPos &DataToMove) *Cat &Null_Char)
CallPrc Prc('strcspn') Parm((&Null_Base) (&Null_Chars)) +
If Cond(&Pos = &DataToMove) Then( +
ChgVar Var(&Pos) Value(0))
Else Cmd(ChgVar Var(&Pos) Value(&Pos + &StrPos))
Assuming that the preceding source is stored in member SCNCHRCPP of source file QCLSRC, you can, if your system is V6R1 or higher, use the following command to create the SCNCHRCPP program:
If your system is V5R4, you will need to use the two following commands to create the SCNCHRCPP program:
CrtPgm Pgm(ScnChrCPP) BndDir(QC2LE)
If your system is earlier than V5R4, then the preceding source will not compile successfully due to the use of *Defined storage. You will need to change the source shown to remove the use of defined storage, which can be done, but I'm not going to show the necessary changes in this article as it's time for you to move to V5R4 (or later).
While the general flow of SCNCHRCPP is similar to the source of last month's CHKCHRCPP program, there are some significant differences. With two exceptions, the changes are related to our support of the STRPOS keyword. The two exceptions are 1) some rather trivial changes to variable names (for instance, &Comparator to &Chars) and 2) SCNCHRCPP is calling the Find Offset of First Character Match (strcspn) API rather than the strspn API used last month. Documentation for the strcspn API can be found here.
In order to support the STRPOS function of the SCNCHR command, the following changes to last month's CHKCHRCPP program have been made:
A fourth parameter, &STRPOS, has been added to the PGM command. &StrPos is defined as an unsigned integer.
The structure &MsgDta has been added. This structure is used to provide replacement data within an error message when the user-specified starting position (&StrPos) exceeds the length of the Base string being passed to SCNCHRCPP. The two variables included in the message are the user-specified starting position (&MsgStrPos) and the actual length of the Base string (&Len). The message that will be sent is ESC0003. To create this message into message file OURMSGS (a message file we've used in past articles), you can use the following command.
AddMsgD MsgID(ESC0003) MsgF(OURMSGS) +
Msg('STRPOS value of &1 is greater than the length of BASE value (&2)') +
Fmt((*UBin 4) (*UBin 4))
In terms of processing, SCNCHRCPP first uses the RTVVARLEN command to determine the blank-trimmed length of the parameter &Base. If the &StrPos parameter value is greater than this length, then the ESC0003 escape message we created earlier is sent and the program ends.
The next change is related to determining the characters of &Base that are to be scanned. In order to substring out the relevant characters of &Base, SCNCHRCPP first calculates the number of characters to scan. This is done by subtracting the STRPOS value from the blank-trimmed length of variable &Base (&Len), adding 1, and then setting this value to the variable &DataToMove. The program then sets the variable &Null_Base to the substring of &Base, starting at &StrPos and for a length of &DataToMove, concatentated with a null byte (&Null_Char). Last month's program, CHKCHRCPP, used the *TCat operation when concatenating &Null_Char, and while we could have used *TCat in SCNCHRCPP, it would also be a bit wasteful. As we already know the blank-trimmed length (&Len) and that's all we're substringing out of &Base, there is no need for *TCat to recalculate the blank-trimmed length. So we simply use *Cat.
Having set &Null_Base to the null-terminated data that is to be scanned, CHKCHRCPP calls the strcspn API. The API returns the relative location of the first match within &Null_Base to any character that is found in the variable &Null_Chars. If the returned value &Pos is equal to the length of the scanned string (&DataToMove), then no matching characters were found and the SCNCHR command returns a value of 0 for the variable associated with the SCNCHR POS keyword. Otherwise, SCNCHR returns the location within &Base where the first match was found. Note that this returned location is relative to the original value of &Base, not to the user-specified &StrPos starting position. Returning the original relative position is in keeping with the RPG built-ins %Scan and %Check and is accomplished by simply adding &StrPos to the strcspn returned &Pos value.
Testing the SCNCHR Command
Similar to last month's USECHKCHR test program, here is a test program for the SCNCHR command.
Pgm Parm(&Chars_In &Base_In &StrPos_In)
Dcl Var(&Chars_In) Type(*Char) Len(32)
Dcl Var(&Base_In) Type(*Char) Len(32)
Dcl Var(&StrPos_In) Type(*Dec)
Dcl Var(&Pos) Type(*UInt)
Dcl Var(&Pos_Char) Type(*Char) Len(5)
ScnChr Chars(&Chars_In) Base(&Base_In) Pos(&Pos) +
MonMsg MsgID(ESC0003) Exec(Do)
SndPgmMsg Msg('Invalid starting position') +
If Cond(&Pos *NE 0) Then(Do)
ChgVar Var(&Pos_Char) Value(&Pos)
SndPgmMsg Msg('Character ' *Cat +
%sst(&Base_In &Pos 1) *Cat +
' found at ' *Cat +
Else Cmd(SndPgmMsg Msg('No characters found') +
The program is based on USECHKCHR but does have the addition of a MONMSG for message ESC0003.
Assuming that the preceding source is stored in member USESCNCHR of source file QCLSRC, then you can use the following command to create the USESCNCHR program.
From the command line, we can now test a few scenarios.
Entering the command Call UseScnChr ('abc' 'cabbage' 1) will result in the message 'Character c found at 00001' as the first character of 'cabbage' (the 'c') is in the list of scanned for values ('abc').
Entering the command Call UseScnChr ('abc' 'cabbage' 6) will result in the message 'No characters found' as the sixth and seventh characters of 'cabbage' ('ge') are not within the list of scanned for characters ('abc').
Entering the command Call UseScnChr ('abc' 'cabbage' 100) will result in the message 'Invalid starting position' as the string 'cabbage' is only seven characters in length and we asked for a starting position of 100 (which doesn't exist).
Entering the command Call UseScnChr ('abc' 'a cabbage' 6) will result in the message 'Character b found at 00006'. While there is also a 'b' in the fifth position of 'a cabbage' that 'b' is bypassed due to the starting position being six.
To provide support for a starting position within last month's CHKCHR command, you will need to retrofit the changes found in the SCNCHR command and SCNCHRCPP program to the CHKCHR command and CHKCHRCPP program.
More CL Questions?
Wondering how to accomplish a function in CL? Send your CL-related questions to me at