REXX inside and out - EDM/2 REXX utility library
Written by Joe Wyatt
One of the more powerful weapons in your arsenal of functions in the standard Rexx library is translate(). This function is commonly used to replace characters in one string that match those in a second string with corresponding characters from a third string. You follow all of that? Well, by the time you finish reading this month's column you will not only be able to use this function for simple character translations, but you will have the ability to format data strings such as currency, time, and dates with translate(). But wait; that's not all!
Since this is an international "publication", I could not talk about formatting currency and such without either being very favourable to one country's implementation or giving you a way to get the current country dependent formatting options. With that in mind it is time to introduce the first edition of the EDM/2 Rexx Utility Library.
For supporting ideas and efforts in this column I have put together a (currently) small DLL which houses three external Rexx functions. This library will be expanded as requested by readers or as deemed necessary by me. Please consider this library as "freeware". &deity. knows that you don't have anything left over to support "shareware" after shelling out the money to pay for this free magazine so it is included in the price [grin]. The library can be freely distributed; all I ask is that it not be modified in any way.
This month's column will provide initial documentation for the new Rexx utility library as well as information on effectively using the translate() function.
EDM/2 Rexx Utility Library
The new library contains three functions that should prove useful.
I figured there might be copies of this library scattered about in the future and it might be helpful to check the validity of each copy. You might also use EdmRexxVer to test for a new function's existence. Let's say that I add EdmFutureFunction to the library next month; when this function is needed the version could be tested to see if it is at least "4.3".
version = EdmRexxVer(); /* there are no arguments */
Figure 1: EdmRexxVer() syntax.
Version of the magazine for which this library was modified. That means that even though this is the first edition of the library, the initial return value will be 4.2.
Function will register all public functions to Rexx so that you don't have to.
call EdmLoadFuncs /* there are no arguments */
Figure 2: EdmLoadFuncs() syntax.
0 if successful. 40 if called incorrectly.
/* register all functions contained in EdmRexx.Dll to Rexx */ /* external functions residing in a dll must be registered to */ /* be accessed by the Rexx program. */ call RxFuncAdd "EdmLoadFuncs", "EdmRexx", "EdmLoadFuncs" call EdmLoadFuncs
Figure 3: EdmLoadFuncs() example.
Provide read access to current country dependent information.
rc = EdmCountryInfo( "ctry." );
Figure 4: EdmCountryInfo() syntax.
0 if information returned ok. 40 if there was a problem.
Name of a stem. This stem is updated with the following information:
|ctry.DateFormat|| Possible values:
|ctry.ThousandsSeparator||Character to use in numerics to separate thousands|
|ctry.Decimal||Character to use as the decimal indicator|
|ctry.DateSeparator||Character to use to separate elements in date format|
|ctry.TimeSeparator||Character to use to separate elements in time format|
|ctry.CurrencyFormat|| Possible values:
|ctry.DecimalPlace||Number of digits after the decimal in monetary strings|
|ctry.DataSeparator||Data list separator|
Look for a separate INF file for this library soon. (whenever I get the time)
Last issue's column mentioned that Rexx was a great language for processing strings and full words at a time. That is still true and we will be using translate() to change an entire number with one call, but we must introduce translate() for what it is. Translate() deals with one character at a time. The online Rexx reference manual defines the syntax of the translate() function as follows:
str = translate(string, , , );
Figure 5: translate() syntax.
Let's see what happens when translate() is called with all parameters present. The first action is to check the length of tableout and tablein. If tableout's length is less than that of tablein the pad character is appended to tableout until the lengths match. The default pad character is a space. If tableout's length is greater nothing is done, but it is easier to imagine tableout being truncated to match the length of tablein. Once the tables are satisfactory a copy is made of the value in the variable string, and the real processing begins.
String is searched for all occurrences of the first character of tablein. If any are found then the corresponding characters in the copy are changed to the first character of tableout. This process continues until string has been searched for all characters in tablein. The result value is the copy of string that was changed with the characters in tableout. Now for the variations.
When there is only one parameter passed the variable tableout defaults to the upper case alphabet, tablein defaults to the lower case alphabet, and pad is a space. This has the effect of translating all lower case characters in the passed parameter to upper case.
When string is specified along with either tablein or tableout, but not both, the return string is the pad character repeated for the length of the original string. This is also the case when only string and pad are specified. I haven't found any use for these permutations but, gosh, isn't it good to know that they exist.
You are now armed with everything you need to know about translate() to understand the next discussion.
Someone somewhere sometime before I got into this business 15 years ago figured out a way to use the translate() function a little differently. (It should be noted here that translate() is not unique to Rexx and even exists as an assembler instruction on some machines.) It seems natural to have your tables be static and the value of string be the real variable.
What would happen if we let string and tablein be the somewhat static pieces of the equation and our main variable be tableout?
String now becomes a template for what the output should look like and tablein maps the current positions of characters in tableout, our variable. Let's pick apart an example to see how this might work.
str = translate("$abc,def.gh", 27446295, "abcdefgh")
Figure 6: Using translate() to format numbers.
Here, string is searched for the first character in tablein, an "a". This "a" is found in string and the first character of tableout, a "2", replaces the "a" in the copy of string. This makes the value of copy become "$2bc,def.gh". When this is done for the second character of tablein the copy becomes $27c,def.gh. When all characters of tablein have been processed the final result is "$274,462.95". Nice bit of formatting, no? Let's look at one more example.
str = translate("ma/db/yc", "96/02/01", "yc/ma/db")
Figure 7: Using translate() to change date formats.
Table out is a date in yy/mm/dd format, but the desired format is mm/dd/yy. This invocation of translate() will place 02/01/96 into the return variable, str. Now we are ready for the big finale.
Country Independent Currency Formats
I'm certain that with the above information you can put EdmRexx and translate() together to produce beautiful results, but I'm going to bore you with my solution anyway. We'll pretend that I'm paid by the word and call it an example.
What follows is a detailed discussion of accompanying example command file, money.cmd. This program receives an amount as the argument and formats it as currency for the current country dependencies recorded in OS/2.
The first statement in the program sets the number of digits Rexx can deal with to handle the maximum number that the routine can handle. RxFuncAdd is called next to tell Rexx that the external routine, EdmLoadFuncs, resides in a dll called EDMREXX.DLL. EdmLoadFuncs is then called to register all other functions that are included in the dll to Rexx.
/* money.cmd -- a country independent currency formatter */ numeric digits 27 /* as if I'd ever see this much cash! */ call RxFuncAdd "EdmLoadFuncs", "EdmRexx", "EdmLoadFuncs" call EdmLoadFuncs
Figure 8: MONEY.CMD initialization.
To Rexx, the value of every variable is a string. Datatype(), a builtin Rexx function to check the type of the contents of a variable, is used to ensure that the argument that we are about to format does indeed contain only numeric data.
numb = arg( 1 ) do while ( datatype(numb, "Number") <> 1) say "Please enter a valid number to be formatted as currency:" pull numb end /* do */
Figure 9: Using datatype() to get a number.
EdmRexx is used to gather the specific country information needed complete the format. The name of a stem, X., is passed to be used to build all variables.
call EdmCountryInfo "X."
Figure 10: Getting the country information using EdmCountryInfo().
Our first call to translate() simply replaces the spaces in the variable string to the character that is used to separate thousand groupings in the current country. If the ThousandsSeparator is a comma, as it is in the US then the value of string after the translate() would be "abc,def,ghi,jkl,mno,pqr,stu,vwx".
tablein = "abcdefghijklmnopqrstuvwx" string = "abc def ghi jkl mno pqr stu vwx" string = translate(string, x.ThousandsSeparator, " ");
Figure 11: Preparing to format the number as a currency amount.
The number is divided into whole number and fraction parts in the next section. The length of the whole number section plus thousands separators is placed into the variable len. Its calculation is a bit queer and worth reviewing. It is basically the sum of the length of the whole number portion, length(int), plus the number of thousands groups, lennumb % 3. If it is left here then there will be an extra comma preceding the number when there are even groups of three. The odd looking amount subtracted from the sum, (lennumb // 3 = 0), is an equality test that resolves to 1 if there are even groupings of three and 0 if there are not. This use of the comparison statement is extremely common, but proves quite useful in this case.
For the sake of a complete example let's set a value of "123456.78" for numb. After the following section of code executes "int" is equal to "123456", "lennumb" contains "6", "len" is "7" (length of 6 plus 2 groups of three minus 1 because they are even groups), and "fract" has a value of "78".
int = trunc(numb) lennumb = length(int) len = lennumb + lennumb % 3 - (lennumb // 3 = 0) fract = numb - int
Figure 12: Determining the number of digits to the left of the decimal.
The next section contains the meat of this column. Tablein is truncated on the left to the length to contain the same number of letters as our integer has digits. String is truncated on the left to the length of the integer plus the number of commas that need to be inserted. String now serves as a template for what the number should look like. "whole" is then created with the result of the formatting translate().
Using the same numbers from our previous example "tablein" will be "stuvwx", the template, "string", will be "stu,vwx", and "whole" will contain "123,456" after the next three statements.
tablein = right(tablein, lennumb) string = right(string, len) whole = translate(string, int, tablein)
Figure 13: Preparing to format, take 2.
The final section of code puts all of the pieces together based on the value of the CurrencyFormat value of the compound variable created by EdmCountryInfo. The only real point of interest below is the invocation of substr. Substr is used to ensure the fraction value of the monetary amount contains the proper number of digits according to the country dependent value of DecimalPlace. The function left() could also have been used for this purpose.
Completing our running example and assuming US code page the returned string would be "$123,456.78".
select when (x.CurrencyFormat = 0) then /* currency indicator goes is at beginning */ final = x.Currency || whole || x.Decimal || substr(fract, 3, x.DecimalPlace, '0'); when (x.CurrencyFormat = 1) then /* currency indicator goes is at ending */ final = whole || x.Decimal || substr(fract, 3, x.DecimalPlace, '0') || x.Currency; when (x.CurrencyFormat = 2) then /* currency indicator replaces decimal */ final = whole || x.Currency || substr(fract, 3, x.DecimalPlace, '0'); end return final
Figure 14: Formatting the number properly.
All Said and Done
Even if you knew everything there was to know about the translate() function before you started reading this column, you at least have gained a new toy. EdmRexx will grow in the future. All you have to do is ask. Let's face it, if you are going to wait on me to have all of the original thoughts for this column we are going to go downhill fast. Let me know of any problems or successes you have. I have only tested this code in one country. If you'd like to send me tickets to Bermuda I'd be happy to test it there as well. [wink]