Solution to Exercise 2

The first real coding exercise consists in writing the VAT EXEC.

This was the assignment

  1. Get the file PRICE LIST ©  The layout of the records is as follows:
        01-07 Partnumber
        09-40 Description
        42-50 List price
    
  2. Write a procedure named VAT EXEC that
  3. You will encounter at least one problem with one record containing invalid data.  Use debugging techniques to discover why it fails.

General remarks.

Single Exit Point

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:

  1. Your procedure may be called by another one that expects to get a return code indicating whether yours ran well or not.
  2. Users may start your procedure from the FILELIST cmd area.  Even when your procedure displays an error message, the user has to clear the screen to return to FILELIST, whereby your message gets lost.  FILELIST however will still display an error code, such as *28.

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.

Reading files

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. 

Avoid variables

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. 

Closing files

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.

EXECIO parameters

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.

Arithmetic functions

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.

Optimize your code

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

Commented solutions

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

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

Use backward navigation button to return to the assignments.