Midware Ltd.

Date & Time Support

Home
Services
News
AS/400
Employment

Sign-up for e-mail notifications

Take our weekly poll

Dow Jones Intraday

Nasdaq Intraday

 

 

As of V3R1, the AS/400 and RPG now support date and time data types.  Prior to this support, numeric fields were often used to represent date and time data.  Custom written programs then had to be developed to manipulate, validate, and interpret dates.

With date and time fields comes a whole host of added support to validate, format, compare, and manipulate dates and times.  You may still have the need to write some custom date/time routines, but development of these will be greatly simplified using the new data types.


Declaring Date/Time Fields

Date and time data type fields may defined either in physical files using DDS, or as internal fields in RPG (using D-specs).  Timestamp fields, which include both date and time (including micro-seconds), may also be defined.

Type Data type code
Date D
Time T
Timestamp Z

When defining date fields in D-specs, the INZ keyword may be used to assign an initial value to the date/time field, as in:

DName+++++++++++ETDsFrom+++To/L+++IDc.Keywords++++++++++++++++++++++++++++
D @CurrDate       S               D   inz(d'2000-01-01')
D @CurrTime       S               T   inz(t'23:59:59')
D @CurrTimestmp   S               Z   inz(*sys)

Note that a value of *SYS may be specified to initialize a date, time, or timestamp to the current system date/time.

Date and time fields may be defined in a number of different formats.  The format determines how date and time field is displayed and printed.  Use the DATFMT keyword in D-specs or H-specs to define the format of a date field.  The available date formats are (note the separator characters):

DATFMT Format Sample
*MDY mm/dd/yy 12/31/99
*DMY dd/mm/yy 31/12/99
*YMD yy/mm/dd 99/12/31
*JUL yy/ddd 99/365
*ISO ccyy-mm-dd 1999-12-31
*USA mm/dd/ccyy 12/31/1999
*EUR dd.mm.ccyy 31.12.1999
*JIS ccyy-mm-dd 1999-12-31

See IBM's online RPG documentation for a complete description of date formats, as well as minimum and maximum values for each format.

The TIMFMT keyword is used in D-specs or H-specs to define the format of a time field.

TIMFMT Format Sample
*HMS hh:mm:ss 13:59:00
*ISO hh.mm.ss 13.59.00
*USA hh:mm am
hh:mm pm
1:59 pm
*EUR hh.mm.ss 13.59.00
*JIS hh:mm:ss 13:59:00

There is only one format available for timestamp fields:

CCYY-MM-DD-hh.mm.ss.xxxxxx

The last 6-positions of the timestamp field are microseconds (timestamp fields are often used to generate unique keys for a file).

When declaring date or time fields in an RPG program, if DATFMT or TIMFMT is not specified on the D-spec, the formats will default to the DATFMT and TIMFMT specified on the H-spec.  If these keywords are not defined on the H-spec, the format will default to *ISO.


Validating Date/Time values

It's one thing to be able to initialize date/time fields at compile time, but in the real world, more often than not, we need to populate date/time fields with run-time data.

As of V4R5, display files don't support date or time data-type fields.  Therefore, if we need to allow a user to enter a date or time, we must first define a numeric or character field, test the field for a valid value, and then move that value to a date or time field.

Date fields are stored internally (in memory) as an integer representing a number of days from a given base date.  Sometimes this format is referred to as super-julian.  When ever the date is to be displayed, printed, written to a file, etc, an algorithm is run to convert the date from an integer to the specified format.

This is the same way date fields are stored in most PC applications as well.  To illustrate this, open a workbook in Microsoft Excel.  Enter "12/31/1999" (without quotes) in a cell. It will be interpreted and displayed as a date.   

Now, change the format of the cell to General.  The value displayed now will be 36525, which is the number of days from January 1, 1900 - the base date used in Excel.  Change the number to 2, and re-format the cell as a date (make sure to include year).  The date now displayed will be January 2, 1900.

This is actually very similar to the method the AS/400 uses to store dates.  One limitation of Excel however is that dates before 1/1/1900 are not supported.  Depending on the date format used, the AS/400 will support dates as early as 1/1/0001 and as late as 12/31/9999.  Time and timestamp fields are stored in a similar (slightly more complex) manner.  

Note:  The exception to this is with date formats the utilize a two-digit year (*mdy, *ymd, *dmy, and *jul).  These dates have a valid range of January 1, 1940 through December 31, 2039.  Moving a field with a 4-digit year outside of this range to these fields will cause a runtime exception.  If there is any possibility that an application will use dates outside of this range two-digit year date formats should not be used.

Because of the way dates and times are stored internally, the AS/400 is very intolerant of bad data.  If you try to move an invalid date to a date field you will immediatly get an error.  This is an important consideration.  Many applications use special values of all 9's or all 0's to indicate certain meanings.  For example, a program may assign a value of 99999999 to a Date-Processed field to indicate the records has not been processed yetIf you attempt to move these values to a date field, you will get a run-time exception.

Therefore, if there is any question about the validity of a date or time, you should use the TEST opcode.  TEST will check for a valid date, time, or timestamp format in a numeric or character field.  An opcode extender of D (date), T (time), or Z (timestamp) is used to indicate the type of data being tested for.  Use factor-1 to indicate the format you're testing for, and the result field for the data being tested.  An error indicator will be turned on if the data is invalid.  Some examples:

CL0N01Factor1+++++++Opcode(E)+Factor2+++++++Result++++++++Len++D+HiLoEq
C                   eval      @Num = 123199
C     *mdy          test(d)                 @Num                   99
 ***                          *in99 = *off
C     *ymd          test(d)                 @Num                   99
 ***                          *in99 = *on (99 is not a valid day)
C                   eval      @Num = 20010229
C     *iso          test(d)                 @Num                   99
 ***                          *in99 = *on (2001 is not a leap year)
C                   eval      @Num = 010101
C     *iso          test(d)                 @Num                   99
C                             *in99 = *off
C     *hms          test(t)                 @Num
C                             *in99 = *off

Note:  The built-in-function %ERROR may also be used to check the result of a test operation.  Built-in-functions will be covered in the next section.

When the TEST opcode is used to check a character field, the separator character is also tested.

CL0N01Factor1+++++++Opcode(E)+Factor2+++++++Result++++++++Len++D+HiLoEq
C                   eval      @String = '12/31/99'
C     *mdy          test(d)                 @String                99
C                             *in99 = *off
C                   eval      @String = '123199'
C     *mdy          test(d)                 @String                99
C                             *in99 = *on (invalid separators)

Note that the default date separator may overridden in D-specs or H-specs with the DATSEP keyword.

If the TEST opcode fails, attempting to move the data to a date or time field will also fail.  If it does not fail, the data may be safely moved to a date/time field.


Working with Date/Time data

Once you've verified that a character or numeric field has valid date or time data in it, you may move it to a date or time data type using the MOVE operation code.

D @Date           S               D   datfmt(*usa)
D @Num            S              8P 0 inz(12311999)

C     *usa          test(d)                 @Num                   99
C                   if        *in99 = *off
C                   move      @Num          @Date
C                   endif

Note that in this example, @Num is in the same format as @Date (*usa).  The move operation will assume that factor-2 and the result field are the same format.  If this is not the case, factor-1 must be used to indicate the format of the source data.  For example:

D @Date           S               D   datfmt(*iso)
D @Num            S              8P 0 inz(12311999)

C     *usa          test(d)                 @Num                   99
C                   if        *in99 = *off
C     *usa          move      @Num          @Date
C                   endif

By coding *usa in factor-1 of the MOVE statement, we are telling the compiler to convert the data in @Num from the *usa format to whatever format @Date is defined as.  After this operation, @Date will contain the value '1999-12-31'.  It is generally considered good practice to always code factor-1 on date move operations.  By using the same format on the MOVE as you did on the TEST, your are guaranteed to always have a successful move.

Note:  The EVAL opcode may not be used to move character or numeric data to a date/time field.  Keep in mind that the left-hand and right-hand side of EVAL expressions must be the same data type.

Once data is moved to a date/time field, it can be manipulated or compared to other date/time fields easily.  Dates can be compared to each other without concern for the format.  For example a date in format ccyy/mm/dd can accurately be compared to a date in format mm/dd/ccyy.

Note:  Again, the exception to this rule is comparing two-digit year formats to four-digit formats.  Because dates before 1940 or after 2039 are not supported with two-digit year formats, comparing or moving a four-digit year format outside of this range will cause unexpected results.

Elements from date or time fields (month, seconds, etc.) may be extracted from to a numeric field using the EXTRCT operation code.  Factor-2 should contain the date or time field, followed by ":" and a duration code.  Valid duration codes are *YEARS, *MONTHS, *DAYS, *HOURS, *MINUTES, *SECONDS, *MSECONDS.  Abbreviated duration codes may also be used:  *Y, *M, *D, *H, *MN, *S, *MS.

C                   extrct    @Date:*m      @Month
C                   extrct    @Time:*hours  @Hours

Dates may also be manipulated by using the ADDDUR and SUBDUR operation codes (add/subtract duration).

 *** Add 1 month to @Date
C                   adddur    1:*month      @Date
 *** Set @DueDate to 90 days after @Date
C     @Date         adddur    90:*days      @DueDate

SUBDUR has the same format and functionality as ADDDUR with one additional benefit.  If date fields are used in both factor-1 and factor-2, it will calculate the duration between the two dates and update the result field.  The result field may be calculated as any one of the valid duration codes.

 *** Calculate number of days between @FromDate and @ToDate
C     @ToDate       subdur    @FromDate     @NbrDays:*d
 *** Calculate number of seconds between @FromTime and @ToTime
C     @ToTime       subdur    @FromTime     @NbrSecs:*s

One thing to keep in mind is that using ADDDUR and SUBDUR on dates with durations other that *DAYS may cause unexpected results.  For example, look at the following code:

D @Date           S               D   inz(d'2000-01-31')

C                   adddur    1:*month      @Date
 *** @Date will be 2000-02-29

C                   subdur    1:*month      @Date
 *** @Date will now be 2000-01-29

Even though we added one month and then immediately subtracted one month from a date, the result is not the same as what we started with.  While not necessarily wrong, peculiarities like this should be understood before using this functionality.


Summary

As mentioned earlier, RPG's built in date and time support probably won't replace all your custom date routines, but it will make developing these date routines much easier.  Following are some examples of how you can simplify previously mundane programming tasks.

Checking for leap year

This used to involve a series of division statements (first by 4, then by 100, then by 1000) with work fields to capture the remainders.  Now it is as easy as checking the year for a valid date of February 29.

The following piece of code may be used to check if the 4-digit field @Year is a leap-year:

C                   eval      @TestDate = (@Year * 10000) + 0229
C     *iso          test(d)                 @TestDate               99

If *in99 is set on, @TestDate is invalid, and @Year is not a leap year.

Last day of month

Determining the last calendar day of a month used to involve arrays containing the number of days in each month and a routine to check for leap year.  Calculating the last day of the month for a year/month combination (ccyymm) can now be accomplished in four lines of code:

C                   eval      @WorkDate = (@YrMth * 100) + 01
C     *iso          move      @WorkDate     @Date
C                   adddur    1:*month      @Date
C                   subdur    1:*day        @Date

The first line of code sets the numeric field @WorkDate to the first day of the month.  The next line of code moves this to a date data-type field (@Date).

The third line of code adds one month to @Date to arrive at the first day of the next month.  The last line subtracts one day from this date to arrive at the last day of @YrMth.

Day of week

There are a number of ways to figure the day of week for a given date.  Prior to RPG date/time support none of these methods were pretty.  The code required now is fairly straightforward to calculate the day-of-week for given date (@Date):

 *** We know that 1/2/2000 is a Sunday
D @BaseDate        S               D   inz(d'2000-01-02')

 *** Figure out how many days there are between the base date and @Date
C     @BaseDate     subdur    @Date         @Days:*d

 *** Divide the number of days by 7 & determine the remainder
C                   eval      @DayOfWeek = %rem(@Days: 7)
 ***  0 = Sunday
 ***  1 = Monday
 ***  2 = Tuesday
 ***  3 = Wednesday
 ***  4 = Thursday
 ***  5 = Friday
 ***  6 = Saturday

You could then easily add an array to retrieve the day of week names.

Note that we used the built-in-function %REM to determine the remainder.  We'll cover built-in-functions in detail in the next section.  The DIV and MVR operation codes could have been used instead, but it is somewhat cleaner with the %REM function.


Even though RPG IV's built in date and time support make programming tasks like the above much simpler, in later sections we'll explain methods to make tasks like this even easier by using ILE procedures.

ILE procedures allow you to bundle your code and create your own versions of built in functions.  Instead of executing a subroutine or calling a separate program, ILE procedures can be called directly in expressions.  The examples above could easily be re-coded so they may be execute like the following:

C                   if        LeapYear(1970)
C                   (do something)

C                   if        @Date <= EndOfMonth(200102)
C                   (do something)

C                   eval      @Message = 'Your birthday falls on ' +
C                                DayOfWeek(@Birthday)

For now, this is just to whet your appetite.  We'll cover the specifics of how to do this in the ILE sections.

  Back to C-specs Next to Built-In-Functions 
 
Home Feedback Contents Search

Send mail to midware@midwareservices.com with questions or comments about this web site.
Copyright 2000 Midware, Ltd.

Last Modified:  August 16, 2000