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