Newsletters   Subscriptions  Forums  Store  Media Kit  About Us  Contact  Search   Home 
fhg
Volume 4, Number 17 -- May 19, 2004

Date Handling in RPG IV


by Joel Cochran

After spending time on RPG-related e-mail lists, like RPGIV@yahoogroups.com or rpg400-l@midrange.com, you find some of the most frequently asked questions are about using dates in RPG IV. This article covers the basics of date handling in RPG IV. Since most of us are not at the most recent level of the operating system, yours truly included, the techniques and methods discussed here will work with V5R1 or later. This article will not cover the use of dates with embedded SQL, which has special considerations.


WHEN IS A DATE A DATE?


Dates typically have been stored as numeric variables. Sometimes we were lucky enough to have separate fields for year, month, and day values, and possibly a century field. But often we were saddled with six- or eight-digit numeric fields holding some form of a date. To make matters worse, some clever designers didn't like either one, so we got seven-digit fields--six digits with a century byte.

Then there are all the character field versions of these, with separators, without separators, with leading zeros, without leading zeros. Compound the situation by considering all the different date formats and the lack of format enforcement, and you begin to get a very bleak view of the "state of the date."

So when handling dates, the question is: “Is it a real date, or a numeric or character field pretending to be a date?” We've already discussed the numeric and character dates, but what is a real date? A real date is a program or a file variable that is actually defined as a date variable. To do this in an RPG IV program, define a variable in a D-spec, with an internal data type of d:

d myDate          s               d

The default value of a date variable is 0001-01-01, or the first day of the first month of the first year.

You'll notice there are no length declarations and no statement about whether the date is numeric or character. That's because a real date is a variable type of its own. A date, regardless of format, is stored by the system in a raw binary manner that only the operating system can access and manipulate. In an RPG IV program, that data is accessed through variables, using a specified format.


DATE FORMATS


To clarify, a date exists independent of its format. The following is a short list of some standard formats. (A complete list is available at the IBM's iSeries Information Center and in the ILE RPG Reference.)

  • *YMD – YY/MM/DD

  • *DMY – DD/MM/YY

  • *ISO – YYYY-MM-DD

  • *USA – MM/DD/YYYY

Note that all these date formats include separator characters. Now let's create our date field again, but this time with a default value:

d myDate          s               d   inz(d'2004-05-01')

There are two things of note here. First, to set the value of a date field with a literal, it must be preceded by the letter d and wrapped in single quotation marks ('). This is also true for comparing date values in conditional statements:

if myDate = d'2005-05-01' ;
 // code
endif ;

Second, the format I've used to initialize the date is *ISO: the default DATFMT (date format) for date literals is *ISO.

The above statements will compile, but consider this:

d myDate          s               d   inz(d'05/01/2004')

If you try to compile this, you will receive an RNF0305 error, stating that “the date literal is not valid,” because the format of the date literal is *USA, but, as noted, the default required is *ISO. You can change this behavior by adding an H-spec for DATFMT:

h DATFMT(*USA)

Now all the date fields in your program require literals to be in the *USA format. Whatever format you use, you must be consistent throughout your program.


A DATE IS A DATE IS A DATE


The important thing to understand so far is that our two dates are equivalent, regardless of format, meaning that a statement such as if myDateISO = myDateUSA would be true. Remember that the operating system stores dates in a binary manner, regardless of format. This makes sense, because the 1st of May 2004 is always the 1st of May 2004: the date itself does not change because you view it in a certain format. That said, you might ask, "what good does a format do?"

Assigning a format to a date field makes outputting the date value as desired very simple. When you output a date, the format of the text outputted will correspond with the DATFMT specified. Test this with the following code sample:

d myDateISO        s               d   datfmt(*ISO) inz(d'2004-05-01')
d myDateUSA        s               d   datfmt(*USA) inz(d'2004-05-01')
d myDateString     s             10a

 /free
    dsply myDateISO ;
    dsply myDateUSA ;
    *inlr = *on ;
 /end-free

When you run this little program you should get the following output:

DSPLY  2004-05-01
DSPLY  05/01/2004

So the DATFMT is very handy for controlling the output of a variable. In fact, you'll notice I didn't even bother to convert the date variables to character first. This is because, when possible, the compiler will do it for you on the fly.

Of course, date fields can be assigned values from other date fields, so if you have a date in *ISO format and you want to display it in *USA, simply move the value into a field defined as *USA and display that field:

d myDateISO        s               d   datfmt(*ISO) inz(d'2004-05-01')
d myDateUSA        s               d   datfmt(*USA) inz(d'2000-03-25')
d myDateString     s             10a

 /free
    myDateUSA = myDateISO ;
    dsply myDateUSA ;
    *inlr = *on ;
 /end-free

Before I go on, I'd like to point out the D-spec for the myDateUSA variable. If you look in the options area, you'll see that I have specified DATFMT(*USA), and yet still used an *ISO formatted literal string for the initial value! At first glance this appears wrong, but when you set the DATFMT of an individual variable, it does not affect the rules for literals discussed above: you will always use literals for assigning and comparing in the format designated on your compile statement. In this case, I have not specified a DATFMT for the compiler, so all of the date literal operations require the *ISO format. In this case, specifying datfmt(*ISO) for the myDateISO variable is an unnecessary redundancy used for illustration.


MAKING DATES FROM NUMERIC VARIABLES


To populate a date variable from something other than a literal string, you have to use the IBM-supplied %date BIF. If used with no parameters, %date will return the current system date.

d myDate           s               d   	

 /free
    myDate = %date();
    // myDate = *the current system date*
    *inlr = *on ;
 /end-free

Imagine you have a numeric variable containing a number representing a date in a YYYYMMDD format:

d myDate8         s              8  0 inz(20040501)

In its numeric version, this is *ISO format, so we can create our date like so:

 /free
    myDate = %date( myDate8 );
 /end-free

Now we have a "real" date field populated with the equivalent of “May 1, 2004”, but if our numeric value was in a different format this wouldn't work. In this case we need to inform the %date BIF what the format of the incoming numeric should correspond to:

d myDate          s               d
d myDate8         s              8  0 inz(05012004)

 /free
    myDate = %date( myDate8 : *USA );
    dsply myDate ;

    *inlr = *on ;
 /end-free

If you compile and run this, you will see that we still get our output in the *ISO format. This is because we did not change the DATFMT of the myDate variable; we only instructed the %date BIF to expect the incoming parameter in the *USA format.

So far we've focused on date formats with four-digit years. While ideally we would all use four-digit years all the time, this isn't very realistic, since there are still a lot of six-digit numeric dates floating around out there pretending to be “real” dates. Not to worry, %date can handle these as well, given that you supply the appropriate format name.

d myDate          s               d
d myDate6         s              6  0 inz(050104)

 /free
    myDate = %date( myDate6 : *MDY );
    dsply myDate ;

    *inlr = *on ;
 /end-free

Of course, 050104 can just as easily be interpreted as *YMD or *DMY. Compile and run the following snippet:

d myDate          s               d
d myDate6         s              6  0 inz(050104)

 /free
    myDate = %date( myDate6 : *MDY );
    dsply myDate ;
    myDate = %date( myDate6 : *DMY );
    dsply myDate ;
    myDate = %date( myDate6 : *YMD );
    dsply myDate ;
    *inlr = *on ;
 /end-free

And you get the following results:

DSPLY  2004-05-01                                                           
DSPLY  2004-01-05                                                           
DSPLY  2005-01-04

Three different dates from the same variable. The lesson here, of course, is to be very cautious with six-digit fields. The other thing to consider is that the valid date range with any two-digit-year date format is limited to a range of years from 1940 to 2039. While this may not seem like a problem, the default for any date field is 0001-01-01, which is out of the range of valid two-digit-year dates.


MAKING DATES FROM CHARACTER VARIABLES


Character variables are used in much the same way, but with some interesting additions. When you use a character variable in the %date BIF, you have to be a little more specific. By definition, a numeric variable cannot contain separator characters, but this is not true for a character variable. As such, you have to instruct the %date BIF whether to expect separator characters in the provided variable. By default the BIF will expect separators. In order to specify no separators, add a zero to the end of the DATFMT name:

d myDate          s               d
d myDateWithSep   s             10a   inz('2005-05-01')
d myDateNoSep     s             10a   inz('20050501')

 /free
    myDate = %date( myDateWithSep : *ISO );
    dsply myDate ;
    myDate = %date( myDateNoSep : *ISO0 );
    dsply myDate ;

    *inlr = *on ;
 /end-free

If you don't refer to the correct format, you will receive an error message: “Date, Time or Timestamp value is not valid (C G D F).” This is a generic escape message for any date conversion problems.


ERROR HANDLING


Inevitably, you will try to use an invalid variable value or a non-corresponding DATFMT parameter when populating a date variable. There are a couple of ways to handle these errors in your programs.

The first way is to test the correctness of the variable value before issuing the %date BIF. You can accomplish this by using the TEST opcode with both the d (date) and e (error) extenders. The d error instructs the TEST opcode to test the validity of the date, and the e opcode will set on the %error BIF if an error occurs--in this case, if the string does not contain valid date information.

d myDate          s               d
d myDateWithSep   s             10a   inz('2004-04-31')

 /free
    test(de) *ISO myDateWithSep ;
    if %error();
      // handle error
    else ;
      myDate = %date( myDateWithSep : *ISO );
    endif ;
    *inlr = *on ;
 /end-free

If %error is *ON, then an error occurred. If *OFF, then the data in the variable is compatible with the DATFMT specified.

The other method is to perform the %date BIF operation inside a MONITOR-ENDMON block:

d myDate          s               d
d myDateWithSep   s             10a   inz('2004-04-31')
d error           s             10a   inz('ERROR!')

 /free
    monitor ;
      myDate = %date( myDateWithSep : *USA );
    on-error ;
     // handle error
      dsply error ;
    endmon ;

    *inlr = *on ;
 /end-free

Now that you have a valid, populated, "real" date field, there are several cool things you can do.


DATE MATH


With real date fields, and some additional supplied BIFs, date math couldn't be any easier. There are BIFs for adding and subtracting days, months, or years: appropriately, these are %days, %months, and %years. Below are some examples of how to use these BIFs with your date variable:

 d myDate          s               d   inz(d'2004-05-01')

 /free
   // myDate = '2004-05-01'
    myDate = myDate + %days(3) ;
   // myDate = '2004-05-04'

    myDate = myDate + %months(1) ;
   // myDate = '2004-06-04'

    myDate = myDate - %years(2) ;
   // myDate = '2002-06-04'

    *inlr = *on ;
 /end-free

CALCULATING DATE DIFFERENCES


Calculating the difference between two dates is also very easy, using another BIF, %diff. This BIF allows you to compare two dates and to calculate the difference in days, months, or years.

d myDate1         s               d   inz(d'2004-05-01')
d myDate2         s               d   inz(d'2004-05-08')
d diff_days       s              2s 0
d diff_months     s              2s 0
d diff_years      s              4s 0

 /free
    diff_days = %diff( myDate2 : myDate1 : *days );
    // diff_days = 7

    diff_months = %diff( myDate2 : myDate1 : *months );  
    // diff_months = 0

    diff_years = %diff( myDate2 : myDate1 : *years );
    // diff_years = 0

    *inlr = *on ;
 /end-free

You can get a negative return value if the first parameter is an earlier date than the second parameter. To avoid this, either make sure that the higher date is always first or use the %abs (absolute value) BIF on the return value:

diff_years = %diff( myDate2 : myDate1 : *years );
diff_years = %abs(diff_years);
dsply diffyears;

You may want to embed this result in a character string. Typically, %char will do this for you nicely:

 /free
     myString = 'There is a difference of ' +
                        %char( %diff( myDate2 : myDate1 : *days ) ) +
                        ' days!' ;
      // myString = 'There is a difference of 7 days!'
 /end-free

By default this will suppress leading zeros. However, if you need leading zeros you may want to use %editc instead of %char, but you will quickly discover something interesting:

 /free
     myString = 'There is a difference of ' +
                        %editc( %diff( myDate2 : myDate1 : *days ) : 'X' ) +
                        ' days!' ;
      // myString = 'There is a difference of 0000000007 days!'
 /end-free

The return field for %diff is really 10 numeric! This means that if you want to use leading zeros, and still expect the correct number of characters, you will need to first move the value into an appropriately sized numeric field and then perform %editc. At first this seems strange, perhaps even silly, but once you realize that this BIF, and most others in this article, also apply to %time and %timestamp values, it is easy to conceive of needing 10 digits returned.


RETRIEVE DATE PORTIONS


The %subdt BIF allows you to extract a portion of a date field, such as the day, month, or year.

d myDate          s               d   inz(d'2004-05-01')
d days            s              2s 0
d months          s              2s 0
d years           s              4s 0
d myString        s            128a

 /free
    days = %subdt( myDate : *days );
    // days = 1

    months = %subdt( myDate : *months );
    // months = 5

    years = %subdt( myDate : *years );
    // years = 2004 

    *inlr = *on ;
 /end-free

You can also use short cuts for the second parameter: *d instead of *days, *m instead *months, and *y instead of *years.

As I discussed with %diff above, using these results in character strings is no problem with %char, but if you use %editc you should be aware that %subdt is going to return a 10-digit numeric.


WHAT YOU CAN'T DO WITH DATES


As nice as date operations are in RPG IV, there are some things you can't do as easily as I'd like. I was first introduced to real dates by programming in Java. Since the variable is really an object in Java, you can easily change the day, month, or year to another value without affecting the rest of the date subfields. Unfortunately in RPG, you have to do some fancy math or construct a new date. You also can't automatically retrieve the day of the week or the name of the day of the week. These features are fairly standard in a lot of other languages, but in RPG you will need to find another solution.

Fortunately these solutions and more are available. There are some nifty SQL solutions, and there are plenty of tools available, including my own xRPG Core Library, available for free download from www.rpgnext.com. In fact, issues with date handling were what originally prompted me to create my library. V5R2 does have some enhancements to date handling, but they primarily revolve around converting from dates to numeric variables.


Joel Cochran is the director of research and development for a small software firm in Staunton, Virginia, and is the author and publisher of www.RPGNext.com. E-mail: jcochran@itjungle.com

Sponsored By
ADVANCED SYSTEMS CONCEPTS

SEQUEL
beats the pants off
Query/400 !

Read the
Top Ten Reasons
why you should switch!

(Click Here)

FREE Trial - Call 847-605-1311
or use our online
Trial Request Form


Editors: Howard Arner, Joe Hertvik, Ted Holt,
Shannon O'Donnell, Kevin Vandever
Managing Editor: Shannon Pastore
Contributing Editors: Joel Cochran, Wayne O. Evans, Raymond Everhart,
Bruce Guetzkow, Marc Logemann, David Morris
Publisher and Advertising Director: Jenny Thomas
Advertising Sales Representative: Kim Reed
Contact the Editors: To contact anyone on the IT Jungle Team
Go to our contacts page and send us a message.

THIS ISSUE
SPONSORED BY:

Advanced Systems Concepts
iTera
WorksRight Sofware
Damon Technologies


BACK ISSUES

TABLE OF
CONTENTS
Date Handling in RPG IV

A Solution to the Numeric Parameter Problem

Read a Data Area As a One-Row Table with SQL

Admin Alert: A Lotus Notes Adjustment for Fighting Spam



Copyright © 1996-2008 Guild Companies, Inc. All Rights Reserved.
Guild Companies, 50 Park Terrace East, Suite 8F, New York, NY 10034
Privacy Statement