• The Four Hundred
  • Subscribe
  • Media Kit
  • Contributors
  • About Us
  • Contact
Menu
  • The Four Hundred
  • Subscribe
  • Media Kit
  • Contributors
  • About Us
  • Contact
  • Guru: TryIT – You’ll Like It

    October 2, 2023 Ted Holt

    I have watched children play Whac-a-mole, but I’ve never played it myself, perhaps because the game unpleasantly reminds me of programs that I have had to work on. I fix one bug, only to see another bug rear its ugly little head. Life’s too short to endure such nonsense. Besides, it is embarrassing for someone to tell me that the program I supposedly just fixed is still broken.

    Suppose you’re working on a 4,000-line RPG program and you comment out lines 650-660. What you don’t realize at the time is that a variable used in line 2755 has to have the value assigned to it in line 654. Instead, it has the value assigned to it on line 1435. Whac-a-mole!

    One thing’s for sure: the law of unintended consequences will be obeyed. For that reason, I’ve thought a lot about two questions. (1) Why does program maintenance sometimes cause undesirable side effects? and (2) What programming practices can I adopt to avoid introducing bugs?

    As for the first question, I’m convinced that the primary reason for undesirable side effects is the use of global variables. These are variables that can be referenced throughout an entire program, as opposed to local variables, which are available to only a part of a program. The early RPG and COBOL compilers that I used supported global variables only.

    If my reasoning is correct, then the answer to the second question is to avoid global variables. And how do I do that? By writing subprocedures. And that is how I write programs. My programs are a collection of subprocedures under the control of a main driver routine. Data is passed between subprocedures through parameters, and working data is defined within the subprocedure so that the rest of the program doesn’t know about it.

    Every subprocedure requires rigorous testing, and that’s difficult to pull off in a large program. How can you be assured that your test data will exercise all possible combinations of input data? I get that assurance from a subprocedure I call TryIt.

    Here’s the TryIt template source member, TRYIT_T.

    **free
    
    // This program . . . (does what?) . . .
    
    ctl-opt option(*srcstmt: *nodebugio) dftactgrp(*no) actgrp(*new);
    
    dcl-c  LINE_LENGTH    132;
    
    dcl-f  qsysprt    printer(LINE_LENGTH);
    
    dcl-ds PrintLine      len(LINE_LENGTH)  qualified  inz;
       xxx                char ( 1);
       *n                 char ( 1);
       yyy                char ( 1);
       *n                 char ( 1);
    end-ds;
    
    *inlr = *on;
    
    // print column headings
          PrintLine.xxx              = 'xxx';
    evalr PrintLine.yyy              = 'yyy';
    
    writeln (PrintLine);
    
    TryIt (  'x'  :   0    );
    
    return;
    
    dcl-proc TryIt;
       dcl-pi *n;
          inxxx                char   (1)         const;
          inyyy                packed (1)         const;
       end-pi;
    
       SomeRoutine     ( inxxx: inyyy );
    
       // print results of call to the subprocedure
       clear Printline;
             PrintLine.xxx              =         inxxx                     ;
       evalr PrintLine.yyy              = %editc( inyyy              : 'X' );
    
       writeln (PrintLine);
    
    end-proc;
    
    dcl-proc SomeRoutine;
       dcl-pi *n;
          inxxx              char   ( 1 )         const;
          inyyy              packed ( 1 )         const;
       end-pi;
    
    
    
    end-proc SomeRoutine;
    
    dcl-proc writeln;
       dcl-pi *n;
          inString   varchar(LINE_LENGTH)   const;
          inPosition uns(3)                 const   options(*nopass);
       end-pi;
    
       dcl-ds   ReportLine   len(LINE_LENGTH)   end-ds;
       dcl-s    Position     uns(3);
    
       if %parms() >= %ParmNum(inPosition);
          Position = inPosition;
       else;
          Position = 1;
       endif;
    
       %subst(ReportLine: Position) = inString;
       write qsysprt ReportLine;
    
    end-proc writeln; 
    

    SomeRoutine is a stub for the procedure that I am developing for a production environment. For lack of a better term, I refer to this as the target subprocedure. If you have a better name, let me know.

    Suppose you and I work for a company that sells products. Each product has a catalog price (also known as the list price), but that price is not necessarily the one a customer will pay. The price at which we agree to sell a product depends on three criteria.

    1. Who the customer is. Type A customers, who account for most of our revenue, get a 10% discount. Type B customers, whose business we also value and want to keep, get a 5% discount. Type C customers, occasional or undesirable customers, get no discount.
    2. How many units the customer buys. He pays full price for onesies and twosies. If he’ll let us ship him a lot of them, we’ll give him a bulk discount.
    3. How soon the customer wants the product delivered. If he wants it tomorrow, there’ll be a surcharge.

    This is an ideal task for a subprocedure. The input parameters are obvious – customer ID, product ID, order quantity, and due date. The subprocedure needs to return a price, which could be done through a parameter or the return value. The subprocedure also needs a way to indicate that an error has occurred, which could be done through a parameter, the return value, or an escape message. To keep it simple, I’ll use parameters for everything. We give it a clever name, such as CalculateUnitPrice.

    dcl-proc CalculateUnitPrice;
       dcl-pi *n;
          inCustomerID         like(CustomerID_t)     const;
          inProductID          like(ProductID_t)      const;
          inOrderQuantity      packed (7)             const;
          inDueDate            date                   const;
          ouPrice              packed (7: 2);
          ouErrorCode          uns (3);
       end-pi;
    

    The first two parameters are based on templates in copybook DATADEFS.

    dcl-s   CustomerID_t       packed (5)       template;
    dcl-s   ProductID_t        char   (6)       template;
    

    So that’s the procedure interface for the target subprocedure. To test this subprocedure, we call it from the TryIt subprocedure. TryIt usually has the same input parameters as the target subprocedure. The output parameters of the target subprocedure become local variables within TryIt.

    dcl-proc TryIt;
       dcl-pi *n;
          inCustomerID         like(CustomerID_t)     const;
          inProductID          like(ProductID_t)      const;
          inOrderQuantity      packed (7)             const;
          inDueDate            date                   const;
       end-pi;
    
       dcl-s  Price            packed (7: 2);
       dcl-s  ErrorCode        uns (3);
    
       CalculateUnitPrice ( inCustomerID: inProductID: inOrderQuantity: inDueDate:
                            Price: ErrorCode);
    

    The main routine needs to call TryIt with parameters that cover every possible test case. I usually don’t test every case from the start. I prefer to use some form of test-driven development (TDD). This means that I develop the target subprocedure a piece at a time. I’ll start by writing the code to validate the input parameters.

    TryIt (  0       :   *blank   :  0 : d'2000-01-01'   );
    TryIt (  100     :   *blank   :  0 : d'2000-01-01'   );
    TryIt (  100     :   'AB-101' :  0 : d'2000-01-01'   );
    
    return;
    
    dcl-proc TryIt;
       dcl-pi *n;
          inCustomerID         like(CustomerID_t)     const;
          inProductID          like(ProductID_t)      const;
          inOrderQuantity      packed (7)             const;
          inDueDate            date                   const;
       end-pi;
    
       dcl-s  Price            packed (7: 2);
       dcl-s  ErrorCode        uns (3);
    
       CalculateUnitPrice ( inCustomerID: inProductID: inOrderQuantity: inDueDate:
                            Price: ErrorCode);
    
    end-proc;
    
    dcl-proc CalculateUnitPrice;
       dcl-pi *n;
          inCustomerID         like(CustomerID_t)     const;
          inProductID          like(ProductID_t)      const;
          inOrderQuantity      packed (7)             const;
          inDueDate            date                   const;
          ouPrice              packed (7: 2);
          ouErrorCode          uns (3);
       end-pi;
    
       clear ouPrice;
       clear ouErrorCode;
    
       // validate the parameters
    
       if inCustomerID <= *zero;
          ouErrorCode = 1;
          return;
       endif;
    
       if inProductID = *blanks;
          ouErrorCode = 2;
          return;
       endif;
    
    end-proc CalculateUnitPrice;
    
    

    That’s enough code to exercise these two tests.

    The return code should be 1, 2, and zero after the calls to TryIt. The price will always be zero because I haven’t written any code to set the price.

    How will you know whether that was indeed the case? Well, you can crank up your favorite interactive debugger. There’s nothing wrong with that. I prefer to build a report. I wrote about this technique a couple of years ago. (See Guru: Quick And Handy RPG Output, Take 2.)

    **free
    
    dcl-c  LINE_LENGTH    132;
    
    dcl-f  qsysprt    printer(LINE_LENGTH);
    
    dcl-ds PrintLine      len(LINE_LENGTH)    template;
       CustomerID         char ( 5);
       *n                 char ( 1);
       ProductID          char ( 6);
       *n                 char ( 1);
       OrderQuantity      char ( 8);
       *n                 char ( 1);
       DueDate            char (10);
       *n                 char ( 1);
       Price              char ( 9);
       *n                 char ( 1);
       ErrorCode          char ( 3);
    end-ds;
    
    /copy copybooks,datadefs
    
    *inlr = *on;
    
    // print column headings
          PrintLine.CustomerID       = 'Cus';
          PrintLine.ProductID        = 'Prod';
    evalr PrintLine.OrderQuantity    = 'Qty ';
          PrintLine.DueDate          = 'Due';
    evalr PrintLine.Price            = 'Price ';
          PrintLine.ErrorCode        = 'Err';
    
    writeln (PrintLine);
    
    TryIt (  0       :   *blank   :  0 : d'2000-01-01'   );
    TryIt (  100     :   *blank   :  0 : d'2000-01-01'   );
    TryIt (  100     :   'AB-101' :  1 : d'2000-01-01'   );
    
    return;
    
    dcl-proc TryIt;
       dcl-pi *n;
          inCustomerID         like(CustomerID_t)     const;
          inProductID          like(ProductID_t)      const;
          inOrderQuantity      packed (7)             const;
          inDueDate            date                   const;
       end-pi;
    
       dcl-s  Price            packed (7: 2);
       dcl-s  ErrorCode        uns (3);
    
       CalculateUnitPrice ( inCustomerID: inProductID: inOrderQuantity: inDueDate:
                            Price: ErrorCode);
    
       // Print the results of calling the subprocedure.
       clear Printline;
       evalr PrintLine.CustomerID       = %editc( inCustomerID    : 'X');
             PrintLine.ProductID        =         inProductID           ;
       evalr PrintLine.OrderQuantity    = %editc( inOrderQuantity : 'L');
             PrintLine.DueDate          = %char ( inDueDate            );
       evalr PrintLine.Price            = %editc( Price           : 'L');
             PrintLine.ErrorCode        = %char ( ErrorCode            );
    
       writeln (PrintLine);
    
    end-proc;
    
    dcl-proc CalculateUnitPrice;
       dcl-pi *n;
          inCustomerID         like(CustomerID_t)     const;
          inProductID          like(ProductID_t)      const;
          inOrderQuantity      packed (7)             const;
          inDueDate            date                   const;
          ouPrice              packed (7: 2);
          ouErrorCode          uns (3);
       end-pi;
    
       clear ouPrice;
       clear ouErrorCode;
    
       // validate the parameters
    
       if inCustomerID <= *zero;
          ouErrorCode = 1;
          return;
       endif;
    
       if inProductID = *blanks;
          ouErrorCode = 2;
          return;
       endif;
    
    
    end-proc CalculateUnitPrice;
    
    dcl-proc writeln;
       dcl-pi *n;
          inString   varchar(LINE_LENGTH)   const;
          inPosition uns(3)                 const   options(*nopass);
       end-pi;
    
       dcl-ds   ReportLine   len(LINE_LENGTH)   end-ds;
       dcl-s    Position     uns(3);
    
       if %parms() >= %ParmNum(inPosition);
          Position = inPosition;
       else;
          Position = 1;
       endif;
    
       %subst(ReportLine: Position) = inString;
       write qsysprt ReportLine;
    
    end-proc writeln;  
    

    Here’s the output.

    Cus   Prod       Qty  Due           Price  Err
    00000              0  2000-01-01      .00  1  
    00100              0  2000-01-01      .00  2  
    00100 AB-101       1  2000-01-01      .00  0  
    

    Look at price and error code. Everything is correct so far.

    Let’s continue to build the pricing routine. Let’s add code to retrieve the necessary customer data and product data.

    dcl-proc CalculateUnitPrice;
       dcl-pi *n;
          inCustomerID         like(CustomerID_t)     const;
          inProductID          like(ProductID_t)      const;
          inOrderQuantity      packed (7)             const;
          inDueDate            date                   const;
          ouPrice              packed (7: 2);
          ouErrorCode          uns (3);
       end-pi;
    
       dcl-s Price          like(ouPrice);
       dcl-s CustomerType   char(1);
    
       clear ouPrice;
       clear ouErrorCode;
    
       // validate the parameters
    
       if inCustomerID <= *zero;
          ouErrorCode = 1;
          return;
       endif;
    
       if inProductID = *blanks;
          ouErrorCode = 2;
          return;
       endif;
    
      // Start with the list price.
    
      exec sql
         select p.price
           into :Price
           from products as p
          where p.id = :inProductID;
    
      if SqlState >= '02000';
          ouErrorCode = 3;
          return;
      endif;
    
      exec sql
         select c.type
           into :CustomerType
           from customers as c
          where c.id = :inCustomerID;
    
      if SqlState >= '02000';
          ouErrorCode = 4;
          return;
      endif;
    
      ouPrice = Price;
    
    end-proc CalculateUnitPrice;
    

    We need additional test cases.

    TryIt (  0       :   *blank   :  0 : d'2000-01-01'   );
    TryIt (  100     :   *blank   :  0 : d'2000-01-01'   );
    TryIt (  100     :   'AB-101' :  1 : d'2000-01-01'   );
    TryIt (  100     :   'smurf'  :  1 : d'2000-01-01'   );
    TryIt (  999     :   'smurf'  :  1 : d'2000-01-01'   );
    TryIt (  999     :   'AB-101' :  1 : d'2000-01-01'   );
    

    Assuming customer 100 and product AB-101 are valid data and 999 and smurf are not, these tests cover all the bases so far.

    Cus   Prod       Qty  Due           Price  Err
    00000              0  2000-01-01      .00  1  
    00100              0  2000-01-01      .00  2  
    00100 AB-101       1  2000-01-01     1.80  0  
    00100 smurf        1  2000-01-01      .00  3  
    00999 smurf        1  2000-01-01      .00  3  
    00999 AB-101       1  2000-01-01      .00  4  
    

    All the error codes are accurate, and the only price retrieved is in fact the catalog price for item AB-101. So far so good!

    I’ll stop here, as I’ve covered all the concepts. If this were a production program, I would add the code to calculate the discount and we’d add the necessary TryIt calls to test each piece of code. Eventually I would have a bug-free pricing routine that works exactly as I want it to. Since it has no global data, I could place it into a program or service program without fear of it causing undesirable side effects.

    “But wait,” I hear you say. “Aren’t the first two parameters defined with global data?” And the answer is no, they are based on global data definitions, and global data definitions are a wonderful way to define data consistently throughout a software application, especially when they’re in a copybook.

    “And,” you continue, “isn’t the use of the QSYSPRT file global?” Yes, of course it is, but it makes the code simpler and lets me concentrate on the task of building the target subprocedure. Because of the global printer file, I can put writeln calls in the target subprocedure if I need to. Of course, I remove them before moving the target subprocedure into the destination production source member.

    Building a subprocedure inside a copy of the TryIt template makes it easy for me to develop a subprocedure that does exactly what it needs to do without affecting the rest of a program, and allows me to easily test the code with all possible input values. That gives me peace of mind.

    Besides, I don’t have time to play Whac-a-mole at work.

    RELATED STORIES

    Guru: Whac-A-Mole

    Guru: Test-Driven Development

    Guru: Quick And Handy RPG Output, Take 2

    Share this:

    • Reddit
    • Facebook
    • LinkedIn
    • Twitter
    • Email

    Tags: Tags: 400guru, FHG, Four Hundred Guru, IBM i, RPG, TryIt

    Sponsored by
    OCEAN User Group

    OCEAN TechCon25 Online

    It’s an Exciting Time for IBM i !

    July 16 & 17, 2025 – ONLINE

    Two virtual days of learning, presented by an outstanding group of IBM’ers and IBM Champions, featuring leading-edge topics.

    FREE for OCEAN members!

    Register NOW!

    Annual (12-month) Individual OCEAN Memberships are $80 and a Corporate Membership is $250. A Corporate Membership would allow your entire company to have full access to the OCEAN website & video library and to attend OCEAN events at member rates. Act now because rates are increasing on August 1, 2025.

    Share this:

    • Reddit
    • Facebook
    • LinkedIn
    • Twitter
    • Email

    It’s Time To Tell Us How It Is And What You’re Doing API Dev Tool Delivers For Trucking Outfit

    Leave a Reply Cancel reply

TFH Volume: 33 Issue: 59

This Issue Sponsored By

  • Rocket Software
  • WorksRight Software
  • ServiceExpress
  • ARCAD Software
  • Manta Technologies

Table of Contents

  • 40 Years Of DB2, But Even More For That No-Name Database Embedded In The System/38
  • API Dev Tool Delivers For Trucking Outfit
  • Guru: TryIT – You’ll Like It
  • It’s Time To Tell Us How It Is And What You’re Doing
  • IBM i PTF Guide, Volume 25, Number 40

Content archive

  • The Four Hundred
  • Four Hundred Stuff
  • Four Hundred Guru

Recent Posts

  • With Power11, Power Systems “Go To Eleven”
  • With Subscription Price, IBM i P20 And P30 Tiers Get Bigger Bundles
  • Izzi Buys CNX, Eyes Valence Port To System Z
  • IBM i Shops “Attacking” Security Concerns, Study Shows
  • IBM i PTF Guide, Volume 27, Number 26
  • Liam Allan Shares What’s Coming Next With Code For IBM i
  • From Stable To Scalable: Visual LANSA 16 Powers IBM i Growth – Launching July 8
  • VS Code Will Be The Heart Of The Modern IBM i Platform
  • The AS/400: A 37-Year-Old Dog That Loves To Learn New Tricks
  • IBM i PTF Guide, Volume 27, Number 25

Subscribe

To get news from IT Jungle sent to your inbox every week, subscribe to our newsletter.

Pages

  • About Us
  • Contact
  • Contributors
  • Four Hundred Monitor
  • IBM i PTF Guide
  • Media Kit
  • Subscribe

Search

Copyright © 2025 IT Jungle