Tuesday, August 8, 2017

Date Handling in RPG IV




WHEN IS 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.
  • *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 
 
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.

Adding days to a Date using RPGLE

Problem: you need to add 15 days to a date stored in a manner where its parts are broken into 4 fields.
Here is the data structure used to capture the 4 fields at the time of the I/O (CC,YY,MM,DD). The DS defines fields over them for use by the calculations, specifically NAD_DATE, NAD_DATE_A and NAD_CCYYMMDD.

The Data Structure
 
     D NAD_DateDS      DS
     D NAD_DATE                1      8S 0
     D NAD_DATE_A              1      8
     D CC                      1      2S 0
     D YY                      3      4S 0
     D MM                      5      6S 0
     D DD                      7      8S 0
     D NAD_CCYYMMDD    S               D   DATFMT(*ISO)  
NAD_DATE spanning positions 1 through 8, acts to contain the ccyymmdd components into a single field.
NAD_DATE_A is the character version of NAD_DATE.
NAD_CCYYMMDD is a work-field in *ISO format. NAD_DATE, in ccyymmdd format, is moved to this field.

The Calculations

//Move the numeric variant of date in NAD_DATE into NAD_CCYYMMDD of type date
//in *ISO format.
     NAD_CCYYMMDD = %date(NAD_DATE);
//Add 15 days to NAD_CCYYMMDD of type date  
     NAD_CCYYMMDD = NAD_CCYYMMDD + %days(15) ;
//Convert 2011-07-03 (*ISO format) to a numeric variant of 20110703. The use
//of "*iso0" tells the %char function to drop the "-" and leave it in *ISO format.
     NAD_DATE_A = %char(NAD_CCYYMMDD:*iso0);
//Convert 20110703 to decimal value
     NAD_DATE = %dec( NAD_DATE_A :8:0);   
Since the field NAD_DATE is an aggregate variant of the elementary fields in the data structure, the fields CC, YY, MM, DD contain the new sub-component values. If the field names are the same as those of the file, then an update operation will persist the values to the database.

No comments:

Post a Comment