The first real coding exercise consists in writing the VAT EXEC.
01-07 Partnumber 09-40 Description 42-50 List price
A well-written procedure should have only one exit point and never terminate with a return code equal to zero when something went wrong.
You may argue that there is no reason to return another error code than zero when the program has already displayed an error message.
Let's give you two reasons to exit with a return code in case of problems:
A simple exit routine can be as follows:
EXIT: ERREXIT: parse arg retc,errmsg if errmsg<>'' then say myname':' errmsg exit retc
Once your procedure discovers an error, statements like these:
if fn = '' then do say 'you have to specify a filename' return end
can be reduced to:
if fn = '' then call emsg 8,'You have to specify a filename'
It saves a lot of typing, avoids a DO-loop and improves readability and performance of the procedure.
Remark that the exit routine has two labels, EXIT: and ERREXIT:. Both can be used for the same code. When your procedure terminates correctly, you can therefore code:
call exit 0
This routine is still very simple. Normally the exit routine will also be used to clean up the environment created by the procedure. It can be used to clean up the stack, detach devices, erase work files, etc.
We will further optimize this exit routine in later lessons.
Although this is one of the main subjects of next lesson, it must be clear right from this moment that reading file records one by one is not good for performance.
It is much better to read all the records in one operation (or at least as much as possible). This is generally one of the easiest ways to dramatically improve the performance of a procedure.
Similarly, it goes faster if you can write all the records in one operation too, though the difference with writing one by one is less visible.
We have already mentioned that you should avoid creating variables that will not be used more than once. This also may improve the performance.
On the other hand, when variables are no longer needed, they could be re-used for other purposes.
In this particular case for example, it is possible to reuse the variables that contain the input records for building the output records. As we just said, it is better to read the file in one shot, and this means you will probably load the records into an array. The elements of the array can then be changed by the procedure and reused to write all the records back in one output operation. This means we win on all fronts: fewer overhead due to creation of extra variables and better performance when reading and writing the file.
This is again covered in detail in next lesson, but note that files should be closed before terminating the procedure. This can be of utmost importance.
Beginners frequently make the same error when coding the parameters for EXECIO. When a file is read, the name of the variable, or the stem, should be specified inside the quotes, thus as a constant ! So, if you would code:
'EXECIO * DISKR PRICE LIST A (STEM' Records. 'FINIS'
then REXX will interpret the statement and replace variables by their actual value before it passes the command to CMS. Of course, if the variable was never initialized, its value would be its own name, and there would be no problem. However, if you later modify your procedure and use the variable for other purposes, strange results can happen. For example,
Records.='' 'EXECIO * DISKR PRICE LIST A (STEM' Records. 'FINIS'
actually removes a parameter from the command, and thus, the name of the stem will become FINIS.
On the opposite, when records are written to a file using the STRING option, the name of the variable containing the record must of course be written outside the quotes as REXX has to replace the variable by its real actual value, as here:
str='This is a record to be written' 'EXECIO 1 DISKW OUTPUT FILE A (STRING 'str
When you use the VAR option, the name of the variable must again be codes as a literal string, as in this example:
str='This is a record to be written' 'EXECIO 1 DISKW OUTPUT FILE A (VAR STR'
Indeed, EXECIO will ask REXX for the value of the variable.
The REXX built-in functions that manipulate numeric data and format the output strings seem not known very well.
For alignment of strings, the left() and right() functions are most appropriate.
For number formatting, we have:
Trunc(num,[n])
returns the integer part of the
number and optionally n decimal digits.
For output in
fixed forms, a further manipulation with left() or
right() may be required.
Note that there is a problem with rounding.
This is what the manual tells:
The number is first rounded according to
standard REXX rules, just as though the operation number+0 had been
carried out.
The number is then truncated to n decimal places (or trailing zeros
are added if needed to make up the specified length)...
If you read only the highlighted text, then you would think that rounding is correct and that you don't have to worry. But, later on, there is following extra note: The number is rounded according to the current setting of NUMERIC DIGITS if necessary before the function processes it.
As the default numeric digits is 9, you'll understand why next example does not give the answer you might expect:
say 3*205/1000 0.615 say trunc(3*205/1000) 0
0.615 rounded to 9 decimal digits gives 0.615000000. Then trunc() just strips of the decimal digits... But, if you code:
numeric digits 2 say trunc(10/3,2)
you will get 3.30 and not 3.33.
Format(num,[b],[a])
returns the
rounded and formatted number, with b characters
for the integer part (padded with leading blanks) and a
characters for the decimal part. This is an example:
format('12.437',4,2) gives ' 12.44'
The advantage of format() is that the rounding is correct, and that further processing is not needed to align the data in columnar outputs.
This function also returns an error message when the result does not fit into the output field. This can be considered an advantage as it allows to detect impossible numbers, but the drawback is that your procedure abends with a rather cryptic message.
This piece of code is not optimized:
vat=20.50 do i=1 to records vatprice=price * vat / 100 ... end
At each iteration CPU cycles are consumed for the division by 100, while it could have been done once before the loop, as in this case:
vat=20.50 / 100 do i=1 to records vatprice=price * vat end
We review two solutions as provided by former students.
! /**********************************************************************/ a ! /* VAT - Created by Rudi */ b ! /* Function: calculate VAT */ c ! /* Parameters: NONE */ d ! /* INPUT file: PRICE LIST C */ e ! /* OUTPUT file: PRICEVAT LIST A */ ! /**********************************************************************/ 1 ! Address Command 2 ! 'EXECIO * DISKR PRICE LIST C 1 (STEM LINE.' 3 ! do i=1 to line.0 4 ! price_without_vat=substr(line.i,42,8) 5 ! if datatype(price_without_vat,NUM)=1 then do 6 ! price_vat=(price_without_vat*0.205) 7 ! price_with_vat=price_without_vat+price_vat 8 ! price_with_vat=format(price_with_vat,,0) 9 ! add_info=right(price_vat,8,' ')' 'right(price_with_vat,8,' ') 10 ! end 11 ! else add_info='Price is not Numeric' 12 ! line_new.i=overlay(add_info,line.i,52) 13 ! 'EXECIO 1 DISKW PRICEVAT LIST A 'i' (string 'line_new.i 14 ! end 15 ! exit
if datatype(price_without_vat,'NUM') then do
However, note the quotes around NUM to avoid that REXX considers it to be a variable with whatever value...
price_vat=format(price_without_vat * 0.205,8,2) add_info=price_vat format(price_vat * price_without_vat,8,0)
'EXECIO 1 DISKW PRICEVAT LIST A 'i' (STRING ', overlay(add_info,line.i,52)
Fred carved this in the rocks...
! /* ========================================================== */ ! /* VAT - created by: Fred Flinstone */ ! /* Stonehouse */ ! /* Drake Street */ ! /* Bedrock */ ! /* Function: calculate VAT on PRICE LIST C */ ! /* Parameters: none (VAT hardcoded, to keep it simple) */ ! /* Options: tracetype (to keep the instructors happy) */ ! /* ========================================================== */ ! /* to keep it simple: no checking on tracetype */ ! /* existance of input file */ ! /* ========================================================== */ 1 ! address command 2 ! parse upper arg '(' traceopt . 3 ! trace(traceopt) 4 ! 'ERASE PRICEVAT LIST A' 5 ! vat = 20.5 6 ! 'EXECIO * DISKR PRICE LIST C (STEM' Record_Num. 'FINIS' 7 ! do i = 1 to Record_Num.0 8 ! price = substr(Record_Num.i,42,9) 9 ! if ^datatype(price,'N') then call settheprice 10 ! vatprice = trunc(price*vat/100) 11 ! Record_Num.i = substr(Record_Num.i,1,50), 12 ! right(vatprice,9), 13 ! right(price+vatprice,9) 14 ! 'EXECIO 1 DISKW PRICEVAT LIST A ( ST' Record_Num.i 15 ! end 16 ! 'EXECIO 0 DISKW PRICEVAT LIST A (FINIS' 17 ! exit ! 18 ! settheprice: 19 ! say 'price' price 'on record' i 'not numeric' 20 ! say 'attempting to correct character O to number 0' 21 ! price = translate(price,'0','O') 22 ! if ^datatype(price,'N') then do 23 ! say 'still no luck ...' 24 ! say 'the price was defaulted to 1000 (which could make you happy)' 25 ! price = 1000 26 ! end 27 ! else say 'OK, the price was corrected to' price 28 ! return
'EXECIO 1 DISKW PRICEVAT LIST A (STRING', substr(Record_Num.i,1,50) right(vatprice,9 right(price+vatprice,9)
Use backward navigation button to return to the assignments.