I Was Just Wondering. . .
December 14, 2011 Hey, Ted
Note: The code accompanying this article is available for download here.
I was wondering if you have come across any techniques using subprocedures to simulate associative arrays in RPG IV. I’ve just started experimenting with the idea. I’d like to create dynamic associative arrays using service programs and user indexes.
As a matter of fact, I had thought about implementing associative arrays in RPG, Steve. I’ll share my thoughts on that topic. But first, let me talk about wondering.
I do a lot of wondering. You’ve seen the results of many of my wonderings in previous issues of this august newsletter, especially the end-of-year issues, when I have written about things like solving sodoku puzzles and finding a path through a maze. Wondering is one of the best skills a programmer can have. I’d go so far to say that wondering is one of the best skills a person of any profession can have, except maybe for tax preparers, bureaucrats, and politicians.
Earlier this year, I wondered about the digital alarm clock that sits by my bed. The numbers are made up of illuminated line segments, like this:
_ _ _ _ | | | _| _| |_| |_ |_| | |_ _| | _|
I wondered if I could write a program that would convert a string of digits into a digital-clock facsimile. Well, I could. I wrote an RPG subprocedure that accepted a string of digits as input and produces three strings of blanks, underscores and vertical bars as output.
After I had that working, I wondered why limit the domain to numerals, so I added some letters, such as these:
_ _ _ | |_ |_ |_| | |_ |_ | | | |_|
And I could do that, too.
Then I wondered if I might be able to create more letters if I added slashes and back slashes.
_ |/ |_| / | | /
And I could do that, too.
Recently I was reading a Web forum just for a change of pace. The messages were written in Esperanto, a language that I can read with the aid of a dictionary but don’t claim to be able to speak. Someone said that he’d been speaking Esperanto for 35 years. However, he failed to end some direct objects with the letter n. I got to wondering, not whether he’d really been using Esperanto for 35 years, because I don’t care about that, but whether I could write a program that would help a fellow catch grammatical errors. After all, Microsoft Word does it.
I decided that writing a grammar checker would take more time and effort, in research and development, than I could devote to it. However, maybe I could write a sort of “visual explain” that would tell the function and part of speech of each word.
And I did. I wrote it in Visual Basic 2010 Express. Here’s an example of the output.
mia adjective fratino noun havas verb, present tense multajn adjective, plural, accusative bonajn adjective, plural, accusative amikojn noun, plural, accusative
I see that every word is evaluated to be what I intended it to be. For example, if any of the last three words did not have “accusative” in the description, I would know that I had omitted that final n. I could add more tests to what I’ve got, and maybe I will someday.
I may never use this program, but if I ever decide to post to an Esperanto forum, I will run my text through my program first. And I doubt seriously I’ll ever use the digital-clock code in production. In fact, I don’t even know where it is at the moment. But I don’t think the time I spent on these little projects was wasted, because I tackled a couple of problems I’d never tackled before.
Now let me return to Steve’s question.
An associative array is a collection of keys and values. We’re accustomed to indexing array elements by position in the list. With an associative array, elements are referenced by names. A good introductory article is The Power of Associative Arrays, by Howard Fosdick.
For example, here’s a REXX procedure in which the values 26 and 28 are assigned to elements Chuck and Mary, respectively, of the age array, and then those values are sent to the standard output device.
age. = 0 age.Chuck = 26 age.Mary = 28 say age.Chuck say age.Mary
The best idea I could come up with was to use two arrays–one to hold the key value and one to hold the data value–and then use the usual array lookup op codes to access the arrays. Something like this:
H dftactgrp(*no) actgrp(*new) H option(*srcstmt: *nodebugio) D ds D ArrayData dim(24) D Name 16a overlay(ArrayData: 1) inz D Age 3p 0 overlay(ArrayData: *next) inz D NbrActive s 10i 0 D AnAge s 3p 0 D SomeGuy s 20a D SetAge pr D inName 20a const D inAge 3p 0 const D GetAge pr 3p 0 D inName 16a const /free *inlr = *on; SetAge ('Bill E. Klub': 29); SetAge ('Nan Tuckit': 18); AnAge = GetAge ('Nan Tuckit'); AnAge = GetAge ('Jerry Mander'); SomeGuy = 'Bill E. Klub'; AnAge = GetAge (SomeGuy); return; /end-free * ============================================ P SetAge b D pi D inName 20a const D inAge 3p 0 const *** locals D Ndx s 10i 0 /free Ndx = %lookup(inName: Name: 1: NbrActive); if Ndx = *zero; NbrActive += 1; Ndx = NbrActive; Name (Ndx) = inName; endif; Age (Ndx) += inAge; /end-free P e * ============================================ P GetAge b D pi 3p 0 D inName 16a const *** locals D Ndx s 10i 0 /free Ndx = %lookup(inName: Name: 1: NbrActive); if Ndx = *zero; return *zero; endif; return Age (Ndx); /end-free P e
The grunt work of storing and retrieving has been relegated to subprocedures. The syntax to store and retrieve ages by name is not as easy as the REXX associative array syntax, but it’s simple enough.
SetAge ('Nan Tuckit': 18); AnAge = GetAge ('Nan Tuckit');
I continued to wonder and came up with something unexpected. I came up with an easy way to add a recap to a report.
I created two related arrays: one to hold a key value, and one to hold an amount of currency. I wrote subprocedures to add to an amount element: one to tell me how many array elements are in use, and one to return an amount by position. (I wrote a subprocedure to get by name, too, but I didn’t need it for this example.) Here’s the RPG source code for my module.
H nomain H option(*srcstmt: *nodebugio) D ds D ArrayData dim(24) ascend D Codes 16a overlay(ArrayData: 1) inz D TotalDue 7p 2 overlay(ArrayData: *next) inz D NbrOfActiveCodes... D s 10i 0 D/copy prototypes,associativ * ============================================ P AddQty b export D pi D inCode 16a const D inQty 7p 2 const *** locals D Ndx s 10i 0 /free Ndx = %lookup(inCode: Codes: 1: NbrOfActiveCodes); if Ndx = *zero; NbrOfActiveCodes += 1; Ndx = NbrOfActiveCodes; Codes (Ndx) = inCode; sorta %subarr(Codes: 1: NbrOfActiveCodes); Ndx = %lookup(inCode: Codes: 1: NbrOfActiveCodes); endif; TotalDue (Ndx) += inQty; /end-free P e * ============================================ P GetQty b export D pi 7p 2 D inCode 16a const *** locals D Ndx s 10i 0 D Qty s 7p 2 /free Ndx = %lookup(inCode: Codes: 1: NbrOfActiveCodes); if Ndx = *zero; return *zero; endif; return TotalDue (Ndx); /end-free P e * ============================================ P GetQtyByPosition... P b export D pi D inNdx 10u 0 const D ouCode 16a D ouQty 7p 2 *** locals /free if (inNdx = *zero) or (inNdx > NbrOfActiveCodes); clear ouCode; clear ouQty; endif; ouCode = Codes (inNdx); ouQty = TotalDue (inNdx); /end-free P e * ============================================ P GetNbrActive b export D pi 10u 0 /free return NbrOfActiveCodes; /end-free P e
Notice that the AddQty subprocedure accumulates based on key value. If it gets a key it’s never seen before, it sets aside another array element and sorts the array.
I created the module and created a service program from it. Then I wrote a calling program.
H option(*srcstmt: *nodebugio) Fqcustcdt if e disk usropn prefix(in_) Fqad3063p o e printer usropn prefix(ou_) D RecapSize s 10u 0 D Ndx s 10u 0 D/copy prototypes,associativ /free *inlr = *on; open qcustcdt; open qad3063p; write header; write space; dow '1'; read cusrec; if %eof(); leave; endif; ou_Name = in_LstNam + ' ' + in_Init; ou_City = in_City; ou_State = in_State; ou_BalDue = in_BalDue; ou_ChgCod = in_ChgCod; write Detail; AddQty ('Chg code: ' + %editc(in_ChgCod:'X'): in_BalDue); AddQty ('State: ' + in_State: in_BalDue); enddo; write space; RecapSize = GetNbrActive(); for Ndx = 1 to RecapSize; GetQtyByPosition (Ndx: ou_SumDesc: ou_Balance); write Summary; endfor; close *all; return; /end-free
Building two recaps is a simple matter of calling the AddQty routine twice. Printing the recaps is a trivial matter of calling GetQtyByPosition within a loop. The report looks like this:
VERY IMPORTANT REPORT Henning G K Dallas TX 3 37.00 Jones B D Clay NY 1 100.00 Vine S S Broton VT 1 439.00 Johnson J A Helen GA 2 3,987.50 Tyron W E Hector NY 1 Stevens K L Denver CO 1 58.75 Alison J S Isle MN 3 10.00 Doe J W Sutter CA 2 250.00 Thomas A N Casper WY 2 Williams E D Dallas TX 1 25.00 Lee F L Hector NY 2 489.50 Abraham M T Isle MN 3 500.00 Chg code: 1 622.75 Chg code: 2 4,727.00 Chg code: 3 547.00 State: CA 250.00 State: CO 58.75 State: GA 3,987.50 State: MN 510.00 State: NY 589.50 State: TX 62.00 State: VT 439.00 State: WY .00
Not bad for a guy who was just goofing off. I’ve attached the code, in case you want to play with it.
To make a generalized service program to handle associative arrays, as Steve suggested, seemed out of the question. However, I know someone else who has taken this idea of associative arrays even further, and if I can manage to find the time to edit his submission, I will publish his article and blow off your toupee.
Wondering is a good thing, and people ought to do more of it. May we all find a heightened sense of wonder in 2012.