Wednesday, February 17, 2010

Date Handling in RPG IV

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.