Date Handling in RPG IV
May 19, 2004 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