20
Mon, May
3 New Articles

Dynamic Arrays Revisited

RPG
Typography
  • Smaller Small Medium Big Bigger
  • Default Helvetica Segoe Georgia Times

A few months ago, I discussed a technique describing how to declare arrays in RPG IV that had a variable number of array elements. Arrays of this nature are referred to as dynamic arrays. This week, I am presenting a new technique for declaring dynamic arrays. This one does not have the complexities of the previous technique.

A major shortcoming in the previous technique that I illustrated was the need to allocate and reallocate memory dynamically based on a mathematical equation (the number of desired elements multiplied by the length of a single element). In addition, the requirement that the number of array elements currently allocated had to be tracked by the program is undesirable. The technique was useable but not fun.

This time, none of those shortcomings occur. About the only oddity is the use of a pointer, and the use of that pointer isn't complex at all. Here's the outline of this new technique:

  • Declare the array with the BASED keyword.
  • Get a pointer to a user space.
  • Assign that pointer to the pointer in the BASED keyword.

Other than that, you can use the array as if it were dynamic, because it is now automatically growing as you access elements in the array. So, if one time you access five elements and another time you access 5,000 elements, your program will work, and none of the allocate/deallocate issues exist.

First things first. To create a dynamic array, you need to declare the array with the BASED keyword. Within the BASED keyword, specify the name of a field. The field name does not need to exist, and probably should not exit. If it does not exist, the RPG IV compiler automatically generates the correct declaration for it. If it does exist, it must be declared as a data type of pointer (*).

The following Definition statement declares an array named DYNARR and specifies the BASED keyword. The BASED(pArr) keyword identifies the based-on pointer field. Since there is no explicit declaration for that field, RPG IV automatically declares one for you.

.....DName+++++++++++EUDS.......Length+TDc.Functions+++++++++++++++
     D dynArr          S            200A   Dim(32766) BASED(pArr)

The following two Definition statements have the same effect as the previous one; however, the pArr variable is explicitly declared on the first line. Therefore, the compiler does not need to declare one for you. This style is useful for more advanced programming in which, perhaps, you would leverage the pArr variable for more than one use, or you might use this style simply for completeness.

.....DName+++++++++++EUDS.......Length+TDc.Functions+++++++++++++++
     D pArr            S               *   
     D dynArr          S            200A   Dim(32766) BASED(pArr)

Whenever a variable is declared and that declaration contains the BASED keyword (as in the examples above), the compiler does not allocate storage for the variable. That means that if you try to move something into DYNARR, you'll get a runtime error, because no storage has been allocated for the variable. When the BASED keyword is involved, you are telling the compiler that you will allocate the storage for the variable yourself. This could mean using the ALLOC/REALLOC opcodes or simply assigning the address of another variable to the pointer. See the example below.

.....DName+++++++++++EUDS.......Length+TDc.Functions+++++++++++++++
0001 D pData           S               *   
0002 D Data            S             32A   BASED(pData)
0003 D Real            S            128A
   
.....C..n01..............OpCode(ex)Extended-factor2++++++++++++++++
0004 C                   eval      pData = %addr(Real)

In this example, the field DATA is declared as a 32-byte character field with the BASED keyword. The pointer field pDATA is explicitly declared on the prior statement. Initially, no storage is assigned to the pDATA pointer; therefore, the DATA has no storage associated with it.

To assign a value to the pDATA pointer, and consequently to provide storage for the DATA variable, an assignment statement is used (line 4). The %ADDR built-in function returns the memory location (i.e., the address) of the field identified by its first parameter. An address is the only type of data that may be stored in pointers.

Once this assignment is made, the data that has been allocated (automatically by the compiler) for the REAL variable is now also being used for the DATA variable. Overlapping fields? Yes.

Notice the variance in the field lengths. The DATA field is 32 bytes long, whereas the REAL field is 128 bytes long. This is perfectly fine as long as the REAL field is at least as long as the DATA field. If the situation were reversed, however, you'd run into a problem if you attempt to access byte 33 of the 32-byte field.

User Spaces as Dynamic Arrays

The safest way that I've found to dynamically allocate storage for a dynamic array is to not do it at all. That is, come up with a way to make the system safely and automatically allocate the storage for you. After all, isn't that the way a true dynamic array scheme would work if IBM did it for us? The big question is, however, what is there that would do such a thing? It occurred to me that a user space object (*USRSPC) could be just the right solution to this question.

User space objects are what data areas are based on. Space objects have been on this system for over 25 years, and user spaces have been around for as long as the AS/400's been around and then some. So they are a pretty reliable object to use.

By default, user space objects are fixed-size objects, just like a data area. However there are two interesting aspects of user space objects that help solve the dynamic memory problem.

1) User space objects have an attribute that controls whether the user space is fixed-length or variable-length. Changing that attribute to 1 causes the user space to become auto-extending. This means that if you create the user space with a length of 12 bytes and you attempt to read or write to byte 750, the underlying interface automatically extends the user space to at least 750 bytes. You do nothing special; it just happens!

2) Using the QUSPTRUS API, you can retrieve a pointer to a user space object that works and acts just like a pointer from the %ADDR built-in function.

Given these two facts, it occurred to me that I could just do something like this:

.....DName+++++++++++EUDS.......Length+TDc.Functions+++++++++++++++
0001 D dynArr          S            200A   Dim(32766) BASED(pArray)

.....C..n01..............OpCode(ex)Extended-factor2++++++++++++++++
0002 C                   CallP     QusPtrUs(szUS:pArray:apiError)

The field named szUS contains the name of the user space. The field name pArray is the return value that receives the pointer to the user space object's data, and apiError is the standard IBM-supplied API error data structure.

With just two lines of code, you can declare and assign the storage for a dynamically sized array. The best part is that you don't have to worry about deallocating or freeing up the storage for the dynamic array when you finish. Create the user space in QTEMP and forget about it!

The bad news is that if you now use something like the SORTA opcode, the entire array will be sorted and hence extend the user space up to the full size of the array. That may be OK if you're expecting that to happen, but you may get unwanted results if you expected it to only sort the elements with data in them. Obviously, a full IBM-provided solution is needed, such as the rumored %SUBARR built-in function that may allow you to segment an array and work with the dynamically specified from and to elements.

Odds and Ends

The QUSPTRUS API is used to retrieve a pointer to the user space object's data. The APi can be easily called with the traditional CALL/PARM opcodes. But after all this is 2003, not 1983, so why not call it using a prototype? The source for the prototype follows:

.....DName+++++++++++EUDS.......Length+TDc.Functions+++++++++++++++
     D QusPTRUS        PR                  ExtPgm('QUSPTRUS')
     D  szUserspace                  20A   Const                                
     D  pRtnPtr                        *
     D  apierror                     16A   OPTIONS(*VARSIZE)

Remember, the parameter names on a prototype are just placeholders or comments. They are not field declarations. So it doesn't matter what you call them, but you should take advantage of the fact that they are not declarations and use them in lieu of comments. For example, "szUserSpace" helps to signify that the field is character and is supposed to contain the name of a user space.

The apiError parameter is the standard API exception/error data structure. Unfortunately, the APIs lack consistency with respect to this data structure. Some of them require it to be passed as a parameter; on some, it is optional; and on others, there is an alternate format. For our purposes, the apiError data structure's format is declared as follows:

.....DName+++++++++++EUDS.......Length+TDc.Functions+++++++++++++++
     D apiError        DS                  Inz
     D  apiLen                       10I 0 Inz(%size(apiError))
     D  apiRLen                      10I 0
     D  apiMsgID                      7A
     D  apiResv1                      1A   Inz(X'00')
     D  apiErrData                   10A

A Full Example

The example that follows can be compiled on your system and should provide you with an example of how this technique works. But first, you must create a user space with a relatively small size (larger size is OK, but we're only testing at this point). To create a user space, call the CrtUsrSpace procedure (included in the RPG ToolKit) from within your RPG IV program and specify a size of something like 32 bytes or so, as follows:

.....C..n01..............OpCode(ex)Extended-factor2++++++++++++++++
     C                   Callp     CrtUsrSpace(szUS : 32)

If you don't have the RPG ToolKit, you can key in and run the CL command and the RPG IV CPP listed in Figures 2 and 3 at the end of this article. That command performs the same function as the CrtUsrSpace procedure, but does it from within CL. Alternatively, you can call the QUSCRTUS API followed by the QUSCUSAT API to accomplish the same thing.

Once the user space has been created, and the source member listed in Figure 1 is compiled, use the STRDBG command to set a break point on the last RETURN opcode (last line of code). Once the breakpoint is set, exit the debugger using F12 and then call the program. When the line containing the breakpoint is about to be run, the debugger will stop and display the source on the screen. At this point, place the cursor on the "dynArr(1700)" variable displayed in the source window and press F11. You should see "Hello World" in that element of the array.

If you do the math, you'll see that the size of an array element (200 in the example in Figure 1) multiplied by the element number (1,700) comes to 340,000. This means that 340,000 bytes of storage would have been required in order for the array to successfully provide 1,700 elements. But since we are using a user space, we avoided any hand-coded allocation schemes and are indifferent about the number of elements we use.

When I created a 32-byte user space and ran this program, the size of the user space automatically grew to 344,064 on my machine.

     H DftActGrp(*NO)  
     
      **  The following three lines are only used if
      **  the RPG Toolkit (www.rpgiv.com/toolkit) is installed.
      **  They are not needed to make this example work.
      /IF DEFINED(RTK_TOOLKIT)                   
      /COPY TOOLKIT/QCPYSRC,space
      /ENDIF

.....DName+++++++++++EUDS.......Length+TDc.Functions+++++++++++++++
     D QusPTRUS        PR                  ExtPgm('QUSPTRUS')
     D  szUserspace                  20A   Const                                
     D  pRtnPtr                        *
     D  apierror                           Like(apiError)

     D apiError        DS                  Inz
     D  apiLen                       10I 0 Inz(%size(apiError))
     D  apiRLen                      10I 0
     D  apiMsgID                      7A
     D  apiResv1                      1A   Inz(X'00')
     D  apiErrData                   10A

      **  USER SPACE NAME
     D szUS            S             20A   Inz('DYNORAMA  QTEMP')
      **  DYNAMIC ARRAY (Note the "based" keyword)
     D dynArr          S            200A   Dim(32766) BASED(pArr)
     
.....C..n01..............OpCode(ex)Extended-factor2++++++++++++++++
     C                   eval      *INLR = *ON
                   
      ** If you have the RPG ToolKit installed
      ** use it to create the user space object.
      ** If you don't you need to create the user
      ** space before calling this example program. 
      /IF DEFINED(RTK_TOOLKIT)                   
     C                   Callp     CrtUsrSpace(szUS : 32)
      /ENDIF                   
      **  Get a pointer to the user space
     C                   CallP     QusPtrUs(szUS:pArr:apiError)
     C                   if        apiRLen > 0
     C     apiMsgID      DSPLY        
      **  Something happened??? Maybe the user space does not.
     C                   return          
     C                   endif
      **  At this point the array is mapped to a user space
      **  so we can use it just like any other array.
     C                   eval      dynArr(1700) = 'Hello World!'
     C                   return

Figure 1: DYNOARR is a test program to prove dynamic array size theory.

Create User Space Made Easy

In order to create an extendable user space, two APIs must be called: QUSCRTUS (Create User Space) and QUSCUSAT (Change User Space Attributes).

The QUSCRTUS API creates a fixed-length user space at the size specified and allows things like the object attribute and text to be applied. The QUSCUSAT API allows you to change some of the attributes of the user space, including the current size, the initial value (a single character repeated in each byte of the user space), and the extendability option. For some reason QUSCRTUS does not include a parameter that allows the extendability option to be specified, so QUSCUSAT must also be called.

In the RPG ToolKit for OS/400, there are procedures that allow you to easily create, change, and delete user spaces from within RPG. In addition, there are extra commands included, such as CRTUSRSPC, DLTUSRSPC, and CHGUSRSPCA. To provide this capability, I have reproduced the CRTUSRSPC command here, along with the CPP. Essentially, I have expanded the code by removing the calls to the ToolKit procedures and replacing them with calls to the OS/400 APIs mentioned above. So the ToolKit is not required to create user spaces on your system.

Listed in Figure 2 is the command definition source for the CRTUSRSPC CL command. The only required parameter is the first one, USRSPC (user space name). To test the dynamic array size theory, however, you want to make sure you specify the size at something like 32 bytes, rather than the 32k default value. For example, the following CRTUSRSPC command creates a user space named DynoRama in QTEMP with a size of 32 bytes and makes it auto-extendable.

CRTUSRPSC  USRSPC(QTEMP/DYNORAMA) SIZE(32) AUTOEXT(*YES)

 

CRTUSRSPC:  CMD        PROMPT('Create User Space')
             /*  Command processing program is RTKCRTUS  */
             PARM       KWD(USRSPC) TYPE(QUAL) MIN(1) +
                          PROMPT('User Space')
 QUAL:       QUAL       TYPE(*NAME) MIN(1) EXPR(*YES)
             QUAL       TYPE(*NAME) DFT(*CURLIB) SPCVAL((*LIBL) +
                          (*CURLIB)) EXPR(*YES) PROMPT('Library')
             PARM       KWD(SIZE) TYPE(*INT4) DFT(32766) REL(*GT 0) +
                          PROMPT('Size')

             PARM       KWD(OBJATR) TYPE(*CHAR) LEN(10) EXPR(*YES) +
                          PROMPT('Object attribute')
             PARM       KWD(AUTOEXT) TYPE(*LGL) RSTD(*YES) +
                          DFT(*YES) SPCVAL((*YES '1') (*NO '0')) +
                          EXPR(*YES) PROMPT('Auto extend')
             PARM       KWD(INZ) TYPE(*CHAR) LEN(1) RSTD(*NO) +
                          DFT(*NULL) SPCVAL((*NULL X'00') +
                          (*BLANK ' ')) EXPR(*YES) +
                          PROMPT('Initialization character')
             PARM       KWD(AUT) TYPE(*CHAR) LEN(10) RSTD(*YES) +
                          DFT(*LIBCRTAUT) SPCVAL((*LIBCRTAUT) +
                          (*CHANGE) (*EXCLUDE) (*USE) (*ALL)) +
                          EXPR(*YES) PROMPT('Authority')
             PARM       KWD(REPLACE) TYPE(*CHAR) RSTD(*YES) DFT(*NO) +
                          SPCVAL((*NO) (*YES)) EXPR(*YES) +
                          PROMPT('Replace')
             PARM       KWD(TEXT) TYPE(*CHAR) LEN(50) DFT(*BLANK) +
                          SPCVAL((*BLANK ' ')) EXPR(*YES) +
                          PROMPT('Text ''description''')
             PARM       KWD(DOMAIN) TYPE(*CHAR) RSTD(*YES) +
                          DFT(*DEFAULT) SPCVAL((*DEFAULT) (*USER) +
                          (*SYSTEM)) EXPR(*YES) PROMPT('Domain')

Figure 2: This is the command definition source for the CRTUSRSPC command.

To compiler the command definition source listed in Figure 2, specify the following CRTCMD command:

CRTCMD CMD(CRTUSRSPC) PGM(mylib/RTKUSRSPC)

Be sure to replace MYLIB with the name of the library where you've compiled the RTKCRTUS program.

The source code listed in Figure 3 is the CPP for the CRTUSRSPC command. The first few dozen lines are declarations, prototypes for the APIs that are called, and the procedure interface for the program itself. Note that I avoid using the outdated *ENTRY/PLIST opcodes and instead use a procedure interface.

The RTKCRTUS program is fairly straight forward; it calls just two APIs: QUSCRTUS to create the user space and then QUSCUSAT to set the auto-extendability attribute for the user space.

Before running the DYNARR program from Figure 1, be sure to compile and run the CRTUSRSPC command to create the user space.

     H DFTACTGRP(*NO)
      
     D rtkcrtus        PR
     D  szUserSpace                  20A   
     D  nUSSize                      10I 0
     D  szExtAttr                    10A
     D  bAutoExtend                   1N
     D  InitValue                     1A
     D  szPubAut                     10A
     D  szReplace                    10A
     D  szText                       50A
     D  szDomain                     10A

     D QusCRTUS        PR                  ExtPgm('QUSCRTUS')
     D  UsrSpace                     20A   Const
     D  ExtAttr                      10A   Const
     D  nSize                        10I 0 Const
     D  InitValue                     1A   Const
     D  PubAuth                      10A   Const
     D  szTextDesc                   50A   Const
     D  Replace                      10A   Const
     D  api_error                          Like(apiError) OPTIONS(*NOPASS)
     D  szDomain                     10A   Const OPTIONS(*NOPASS)

     D QusCUSAT        PR                  ExtPgm('QUSCUSAT')
     D  RtnLibName                   10A
     D  UsrSpace                     20A   Const
     D  USAttr                       64A   OPTIONS(*VARSIZE)
     D  api_error                          Like(apiError)

     D rtkcrtus        PI
     D  szUserSpace                  20A   
     D  nUSSize                      10I 0
     D  szExtAttr                    10A
     D  bAutoExtend                   1N
     D  InitValue                     1A
     D  szPubAut                     10A
     D  szReplace                    10A
     D  szText                       50A
     D  szDomain                     10A

     D apiError        DS                  Inz
     D  apiLen                       10I 0 Inz(0)
     D  apiRLen                      10I 0
     D  apiMsgID                      7A
     D  apiResv1                      1A   Inz(X'00')
     D  apiErrText                   24A

     D rtnLib          S             10A

      ** The QUSCUSAT data structure
      ** This one is setup up only to change the
      ** auto-extendibility option to '1'.
     D UserSpaceAttr   DS                  ALIGN
     D  nRecdCount                   10I 0 Inz(1)
     D  nAttrKey                     10I 0 Inz(3)
     D  nAttrLen                     10I 0 Inz(%Size(bExtend))
     D  bExtend                       1A   Inz('1')

     C                   eval      *INLR = *ON    
     C                   Callp     QusCRTUS(szUserSpace:szExtAttr:
     C                                nUSSize : InitValue : szPubAut : 
     C                                szText : szReplace : apiError : 
     C                                szDomain )
     C                                
     C                   if        apiRLen = 0 and bAutoExtend
      ** Change the user space to AutoExtend
     C                   CallP     QusCUSAT(rtnLib : szUserspace : 
     C                                   UserSpaceAttr : apiError)
     C                   endif                
     C                   return

Figure 3: Here's the RPG IV source for the RTKCRTUS program of the CRTUSRSPC command.

BOB COZZI

Bob Cozzi is a programmer/consultant, writer/author, and software developer. His popular RPG xTools add-on subprocedure library for RPG IV is fast becoming a standard with RPG developers. His book The Modern RPG Language has been the most widely used RPG programming book for more than a decade. He, along with others, speaks at and produces the highly popular RPG World conference for RPG programmers.


MC Press books written by Robert Cozzi available now on the MC Press Bookstore.

RPG TnT RPG TnT
Get this jam-packed resource of quick, easy-to-implement RPG tips!
List Price $65.00

Now On Sale

The Modern RPG IV Language The Modern RPG IV Language
Cozzi on everything RPG! What more could you want?
List Price $99.95

Now On Sale

BLOG COMMENTS POWERED BY DISQUS

LATEST COMMENTS

Support MC Press Online

$0.00 Raised:
$

Book Reviews

Resource Center

  • SB Profound WC 5536 Have you been wondering about Node.js? Our free Node.js Webinar Series takes you from total beginner to creating a fully-functional IBM i Node.js business application. You can find Part 1 here. In Part 2 of our free Node.js Webinar Series, Brian May teaches you the different tooling options available for writing code, debugging, and using Git for version control. Brian will briefly discuss the different tools available, and demonstrate his preferred setup for Node development on IBM i or any platform. Attend this webinar to learn:

  • SB Profound WP 5539More than ever, there is a demand for IT to deliver innovation. Your IBM i has been an essential part of your business operations for years. However, your organization may struggle to maintain the current system and implement new projects. The thousands of customers we've worked with and surveyed state that expectations regarding the digital footprint and vision of the company are not aligned with the current IT environment.

  • SB HelpSystems ROBOT Generic IBM announced the E1080 servers using the latest Power10 processor in September 2021. The most powerful processor from IBM to date, Power10 is designed to handle the demands of doing business in today’s high-tech atmosphere, including running cloud applications, supporting big data, and managing AI workloads. But what does Power10 mean for your data center? In this recorded webinar, IBMers Dan Sundt and Dylan Boday join IBM Power Champion Tom Huntington for a discussion on why Power10 technology is the right strategic investment if you run IBM i, AIX, or Linux. In this action-packed hour, Tom will share trends from the IBM i and AIX user communities while Dan and Dylan dive into the tech specs for key hardware, including:

  • Magic MarkTRY the one package that solves all your document design and printing challenges on all your platforms. Produce bar code labels, electronic forms, ad hoc reports, and RFID tags – without programming! MarkMagic is the only document design and print solution that combines report writing, WYSIWYG label and forms design, and conditional printing in one integrated product. Make sure your data survives when catastrophe hits. Request your trial now!  Request Now.

  • SB HelpSystems ROBOT GenericForms of ransomware has been around for over 30 years, and with more and more organizations suffering attacks each year, it continues to endure. What has made ransomware such a durable threat and what is the best way to combat it? In order to prevent ransomware, organizations must first understand how it works.

  • SB HelpSystems ROBOT GenericIT security is a top priority for businesses around the world, but most IBM i pros don’t know where to begin—and most cybersecurity experts don’t know IBM i. In this session, Robin Tatam explores the business impact of lax IBM i security, the top vulnerabilities putting IBM i at risk, and the steps you can take to protect your organization. If you’re looking to avoid unexpected downtime or corrupted data, you don’t want to miss this session.

  • SB HelpSystems ROBOT GenericCan you trust all of your users all of the time? A typical end user receives 16 malicious emails each month, but only 17 percent of these phishing campaigns are reported to IT. Once an attack is underway, most organizations won’t discover the breach until six months later. A staggering amount of damage can occur in that time. Despite these risks, 93 percent of organizations are leaving their IBM i systems vulnerable to cybercrime. In this on-demand webinar, IBM i security experts Robin Tatam and Sandi Moore will reveal:

  • FORTRA Disaster protection is vital to every business. Yet, it often consists of patched together procedures that are prone to error. From automatic backups to data encryption to media management, Robot automates the routine (yet often complex) tasks of iSeries backup and recovery, saving you time and money and making the process safer and more reliable. Automate your backups with the Robot Backup and Recovery Solution. Key features include:

  • FORTRAManaging messages on your IBM i can be more than a full-time job if you have to do it manually. Messages need a response and resources must be monitored—often over multiple systems and across platforms. How can you be sure you won’t miss important system events? Automate your message center with the Robot Message Management Solution. Key features include:

  • FORTRAThe thought of printing, distributing, and storing iSeries reports manually may reduce you to tears. Paper and labor costs associated with report generation can spiral out of control. Mountains of paper threaten to swamp your files. Robot automates report bursting, distribution, bundling, and archiving, and offers secure, selective online report viewing. Manage your reports with the Robot Report Management Solution. Key features include:

  • FORTRAFor over 30 years, Robot has been a leader in systems management for IBM i. With batch job creation and scheduling at its core, the Robot Job Scheduling Solution reduces the opportunity for human error and helps you maintain service levels, automating even the biggest, most complex runbooks. Manage your job schedule with the Robot Job Scheduling Solution. Key features include:

  • LANSA Business users want new applications now. Market and regulatory pressures require faster application updates and delivery into production. Your IBM i developers may be approaching retirement, and you see no sure way to fill their positions with experienced developers. In addition, you may be caught between maintaining your existing applications and the uncertainty of moving to something new.

  • LANSAWhen it comes to creating your business applications, there are hundreds of coding platforms and programming languages to choose from. These options range from very complex traditional programming languages to Low-Code platforms where sometimes no traditional coding experience is needed. Download our whitepaper, The Power of Writing Code in a Low-Code Solution, and:

  • LANSASupply Chain is becoming increasingly complex and unpredictable. From raw materials for manufacturing to food supply chains, the journey from source to production to delivery to consumers is marred with inefficiencies, manual processes, shortages, recalls, counterfeits, and scandals. In this webinar, we discuss how:

  • The MC Resource Centers bring you the widest selection of white papers, trial software, and on-demand webcasts for you to choose from. >> Review the list of White Papers, Trial Software or On-Demand Webcast at the MC Press Resource Center. >> Add the items to yru Cart and complet he checkout process and submit

  • Profound Logic Have you been wondering about Node.js? Our free Node.js Webinar Series takes you from total beginner to creating a fully-functional IBM i Node.js business application.

  • SB Profound WC 5536Join us for this hour-long webcast that will explore:

  • Fortra IT managers hoping to find new IBM i talent are discovering that the pool of experienced RPG programmers and operators or administrators with intimate knowledge of the operating system and the applications that run on it is small. This begs the question: How will you manage the platform that supports such a big part of your business? This guide offers strategies and software suggestions to help you plan IT staffing and resources and smooth the transition after your AS/400 talent retires. Read on to learn: