Date and time calculations
 
Many years ago, I needed a way to carry out date and time calculations that I could be certain were exact to the second. That is, given a date and time, I needed to be able to add or subtract n seconds to that date and time and get an exact answer. In order to do this, I wrote a library that uses the built-in IDL CALDAT and JULDAY routines to do date calculations, while I handled the hour, minute, and second calculations myself using integer arithmetic.
This library has found its way into much of my other code. For example, to read the 18Z NCEP Reanalysis temperature data for January 1, 2006, I do the following:
 
   IDL> T = NCEP_P_READ_VAR('T', MAKE_DATE(2006, 1, 1, 18))
 
The NCEP_P_READ_VAR function returns a structure containing the temperature data and ancillary information, such as longitudes, latitudes, altitudes, and units.
The library is based around two named structures that store equivalent information in different forms: {CDATE} and {JTIME}. A {CDATE} structure contains the year, month, day, hour, minute, and second as integers. A {JTIME} structure contains the Julian day number and time of day in seconds, also as integers. The library includes the DATE_TO_TIME and TIME_TO_DATE functions to convert between the two formats. In the discussion below, I will refer to these structures and functions as "time" calculations. Generally you should not manipulate the {CDATE} or {JTIME} structures directly.
The library contains routines to initialize a time structure, compute the differences between two times in seconds, add or subtract a time increment to a time, and read and write times as strings using ISO 8601 format. Following the ISO 8601 standard, all routines pass arguments in a consistent order from most significant to least significant: year, month, day, hour, minute, second. To avoid precision problems, time differences are stored as 64-bit integers.
A few of the utility routines make use of the system variable !TauDSolar, which is the length of the mean solar day in seconds (24 x 60 x 60 = 86400). This line should be added to your startup.pro file if you plan to use the library:
   DEFSYSV, '!TauDSolar', 86400L, 1
 
Generally I use the {CDATE}-structure version of the routines. A sample calculation would look something like this:
   IDL> t1 = MAKE_DATE(2006, 1, 1)        ;Create date and time
   IDL> help, t1, /structure
   ** Structure CDATE, 6 tags, length=24, data length=24:
      YEAR            LONG              2006
      MONTH           LONG                 1
      DAY             LONG                 1
      HOUR            LONG                 0
      MINUTE          LONG                 0
      SECOND          LONG                 0
   IDL> print, MAKE_ISO_DATE_STRING(t1)   ;Print using ISO 8601 format
   2006-01-01 00:00:00
   IDL> t2 = TIME_INC(t1, 1000)           ;Add 1000 s to t1
   IDL> print, t2                      
   {     2006        1        1        0       16       40}
   IDL> print, MAKE_ISO_DATE_STRING(t2)
   2006-01-01 00:16:40
 
The default values for year, month, day, hour, minute, and second are 1, 1, 1, 0, 0, and 0, respectively.
Given a date and time t, the library includes utilities for finding the next day, the last day of the month, the first day of the next month, and the first day of the next year.
To step through one month, starting at 00:00:00 on the first day of the month, with a time step of 6 hours, use the following code snippet:
    t1 = MAKE_DATE(year, month)
    t2 = NEXT_MONTH(t1)
    t  = t1
    WHILE (TIME_DIFF(t, t2) LT 0) DO BEGIN
       ...
        t = TIME_INC(t, !TauDSolar/4)
    ENDWHILE
 
To write times into files, including netCDF files, I generally write them as strings using my MAKE_ISO_DATE_STRING function. This function provides control over the precision with which the time is written and whether to use "human" or "compact" format.
   IDL> PRINT, MAKE_ISO_DATE_STRING(date, /COMPACT)
   20050901T181503
   IDL> PRINT, MAKE_ISO_DATE_STRING(date, PRECISION = 'hour', /UTC)
   2005-09-01 18Z
   IDL> PRINT, MAKE_ISO_DATE_STRING(date, PRECISION = 'hour', /COMPACT, /UTC)
   20050901T18Z
 
One advantage to this library is that it was relatively easy to modify it to use an artificial calendar that has no leap days (for the NCAR climate model, for example). All of the details are hidden from the user in the library. Just create your date using the NO_LEAP keyword. The library does all calculations with "no leap" times using a 365 day year.
The library could also be extended (though I have not needed to do so) to handle greater precision by adding, for example, a microseconds field to the {CDATE} and {JTIME} structures.
As with any IDL structure, you can make an array of structures to handle a list of times. To create a list of times separated by 1 day, use the following code snippet:
 
        t = REPLICATE(MAKE_DATE(2006, 1, 1), 10)
        FOR i = 1, 9 DO t[i] = TIME_INC(t[i-1], !TauDSolar)
 
Note, however, that the library functions only work with scalars, not with arrays.