MAPL_ISO8601_DateTime.F90 Source File


This file depends on

sourcefile~~mapl_iso8601_datetime.f90~~EfferentGraph sourcefile~mapl_iso8601_datetime.f90 MAPL_ISO8601_DateTime.F90 sourcefile~keywordenforcer.f90 KeywordEnforcer.F90 sourcefile~mapl_iso8601_datetime.f90->sourcefile~keywordenforcer.f90 sourcefile~mapl_exceptionhandling.f90 MAPL_ExceptionHandling.F90 sourcefile~mapl_iso8601_datetime.f90->sourcefile~mapl_exceptionhandling.f90 sourcefile~errorhandling.f90 ErrorHandling.F90 sourcefile~mapl_exceptionhandling.f90->sourcefile~errorhandling.f90 sourcefile~mapl_throw.f90 MAPL_Throw.F90 sourcefile~mapl_exceptionhandling.f90->sourcefile~mapl_throw.f90 sourcefile~errorhandling.f90->sourcefile~mapl_throw.f90

Files dependent on this one

sourcefile~~mapl_iso8601_datetime.f90~~AfferentGraph sourcefile~mapl_iso8601_datetime.f90 MAPL_ISO8601_DateTime.F90 sourcefile~mapl_iso8601_datetime_esmf.f90 MAPL_ISO8601_DateTime_ESMF.F90 sourcefile~mapl_iso8601_datetime_esmf.f90->sourcefile~mapl_iso8601_datetime.f90 sourcefile~test_mapl_iso8601_datetime.pf test_MAPL_ISO8601_DateTime.pf sourcefile~test_mapl_iso8601_datetime.pf->sourcefile~mapl_iso8601_datetime.f90

Source Code

! ISO 8601 Date/Time Handling
! This module implements the ISO 8601 standard for date/time strings.
! Specifically, it implements ISO 8601-1:2019, but not ISO 8601-1:2019-2
! Zero-padded strings must be zero padded to their full width, unless
! otherwise specified.
!
!      YYYY
! Years are represented by 4 numerical characters (Y).
! The range is: '0000' to '9999' representing the years 1 BCE ('0000') to
! 9999 CE ('9999').
! Strict ISO 8601-1:2019 compliance disallows years before 1583 CE.
! Strict ISO 8601-1:2019 compliance can be enabled with 'STRICT_ISO8601'.
!
!
!      ±YYYYY
! The optional ±YYYYY extension is not supported.
!
! These date formats are supported.
!      YYYY-MM-DD or YYYYMMDD
!      YYYY-MM (but not YYYYMM)
! YYYY is the year as specified above.
! MM is the zero-padded month from '01' (January) to '12' (December).
! DD is the zero-padded day of the month from '01' (1) to '31' (31).
! The range of allowed DD strings varies according to the actual
! calendar, though the first day is always '01'.
!
! These formats are not supported (calendar week and ordinal day).
!      YYYY-Www	or	YYYYWww
!      YYYY-Www-D	or	YYYYWwwD
!      YYYY-DDD	or	YYYYDDD
!
! Time
! ISO 8601 allows fractional seconds, but it does not limit the fractional
! part to milliseconds. Below, 'sss' represents an arbitrary number of digits.
!      Thh:mm:ss.sss	or	Thhmmss.sss
!      Thh:mm:ss	or	Thhmmss
!      Thh:mm	or	Thhmm
!      Thh
!
! Fully-formed time with time zone. Local time not-supported
!      <time>Z
! These time zone formats are not implemented.
!      <time>±hh:mm
!      <time>±hhmm
!      <time>±hh
!
! Datetime
!      <date>T<time>
!
! ISO 8601 Durations
!      PnYnMnDTnHnMnS
!
! These ISO 8601 Duration formats are not supported.
!      PnW
!      P<date>T<time>
!
! ISO 8601 Interval
!      <start>/<end>
!      <start>/<duration>
!      <duration>/<end>
!      <duration>
!
! Repeating ISO 8601 Intervals are not supported.
!      Rn/<interval>
!      R/<interval>

!#define STRICT_ISO8601 !Uncomment for strict ISO8601 compliance
#include "MAPL_Exceptions.h"
#include "MAPL_ErrLog.h"
module MAPL_ISO8601_DateTime
   use MAPL_KeywordEnforcerMod
   use MAPL_ExceptionHandling
   implicit none

! For testing private methods, leave the following line commented out.
!   private

   public :: convert_ISO8601_to_integer_time
   public :: convert_ISO8601_to_integer_date

   interface operator(.divides.)
      module procedure :: divides
   end interface

   ! Error handling
   integer, parameter :: INVALID = -1

   ! parameters for processing date, time, and datetime strings
   integer, parameter :: NUM_DATE_FIELDS = 3
   integer, parameter :: NUM_TIME_FIELDS = 5
   character(len=10), parameter :: DIGIT_CHARACTERS = '0123456789'
   character, parameter :: TIME_PREFIX = 'T'
   integer, parameter :: MINUTES_PER_HOUR = 60

   ! Timezone offset for Timezone Z
   integer, parameter :: Z = 0

   ! Constants for converting ISO 8601 datetime to integer format
   integer, parameter :: ID_WIDTH = 100
   integer, parameter :: ID_DAY = 1
   integer, parameter :: ID_MONTH = ID_WIDTH * ID_DAY
   integer, parameter :: ID_YEAR = ID_WIDTH * ID_MONTH

   integer, parameter :: IT_WIDTH = ID_WIDTH
   integer, parameter :: IT_SECOND = 1
   integer, parameter :: IT_MINUTE = IT_WIDTH * IT_SECOND
   integer, parameter :: IT_HOUR = IT_WIDTH * IT_MINUTE

   ! Derived type used to store fields from ISO 8601 Dates
   type :: ISO8601Date
      private
      integer :: year_
      integer :: month_
      integer :: day_
   contains
      procedure, public :: get_year
      procedure, public :: get_month
      procedure, public :: get_day
   end type ISO8601Date

   interface ISO8601Date
      module procedure :: construct_ISO8601Date
   end interface ISO8601Date

   ! Derived type for communicating date fields internally
   type :: date_fields
      integer :: year_
      integer :: month_
      integer :: day_
      logical :: is_valid_ = .FALSE.
   end type date_fields

   interface date_fields
      module procedure :: construct_date_fields
   end interface date_fields

   ! Derived type used to store fields from ISO 8601 Times
   type :: ISO8601Time
      private
      integer :: hour_
      integer :: minute_
      integer :: second_
      integer :: millisecond_
      ! Timezone is stored as offset from Z timezone in minutes
      ! Currently, only timezone Z offset is used.
      integer :: timezone_offset_ = Z
   contains
      procedure, public :: get_hour
      procedure, public :: get_minute
      procedure, public :: get_second
      procedure, public :: get_millisecond
      procedure, public :: get_timezone_offset
   end type ISO8601Time

   interface ISO8601Time
      module procedure :: construct_ISO8601Time
   end interface ISO8601Time

   ! Derived type for communicating time fields internally
   type :: time_fields
      integer :: hour_
      integer :: minute_
      integer :: second_
      integer :: millisecond_
      integer :: timezone_offset_
      logical :: is_valid_ = .FALSE.
   end type time_fields

   interface time_fields
      module procedure :: construct_time_fields
   end interface time_fields

   ! Derived type used to store fields from ISO 8601 DateTimes
   type :: ISO8601DateTime
      private
      type(ISO8601Date) :: date_
      type(ISO8601Time) :: time_
   contains
      procedure, public :: get_date
      procedure, public :: get_time
      procedure, public :: get_year => get_year_datetime
      procedure, public :: get_month => get_month_datetime
      procedure, public :: get_day => get_day_datetime
      procedure, public :: get_hour => get_hour_datetime
      procedure, public :: get_minute => get_minute_datetime
      procedure, public :: get_second => get_second_datetime
      procedure, public :: get_millisecond => get_millisecond_datetime
      procedure, public :: get_timezone_offset => get_timezone_offset_datetime
   end type ISO8601DateTime

   interface ISO8601DateTime
      module procedure :: construct_ISO8601DateTime
   end interface ISO8601DateTime

   ! Derived type used to store fields from ISO 8601 Durations
   ! Note that ISO 8601 Duration corresponds to ESMF Interval.
   type :: ISO8601Duration
      private
      integer :: years_
      integer :: months_
      integer :: days_
      integer :: hours_
      integer :: minutes_
      integer :: seconds_
   contains
      procedure, public :: get_years
      procedure, public :: get_months
      procedure, public :: get_days
      procedure, public :: get_hours
      procedure, public :: get_minutes
      procedure, public :: get_seconds
   end type ISO8601Duration

   interface ISO8601Duration
      module procedure :: construct_ISO8601Duration
   end interface ISO8601Duration

   ! Derived type used to store fields from ISO 8601 Interval
   ! Note that ISO 8601 Interval is different than ESMF Interval.
   ! This has not been fully implemented
   type :: ISO8601Interval
      private
      type(ISO8601DateTime) :: start_datetime_
      type(ISO8601DateTime) :: end_datetime_
      integer :: repetitions_ = 1
   contains
      procedure, public :: get_start_datetime
      procedure, public :: get_end_datetime
      procedure, public :: get_repetitions
   end type ISO8601Interval

contains

! NUMBER HANDLING PROCEDURES

   ! Return true if factor divides dividend evenly, false otherwise
   pure logical function divides(factor, dividend)
      integer, intent(in) :: factor
      integer, intent(in) :: dividend
      ! mod returns the remainder of dividend/factor, and if it is 0, factor divides dividend evenly
      if(factor /= 0) then ! To avoid divide by 0
          divides = mod(dividend, factor)==0
      else
          divides = .FALSE.
      endif
   end function divides

   ! Checks if n is in (open) interval: (lower, upper), [not inclusive]
   pure logical function is_between(lower, upper, n)
      integer, intent(in) :: lower
      integer, intent(in) :: upper
      integer, intent(in) :: n
      is_between = n > lower .and. n < upper
   end function is_between

   ! Check if c is a digit character
   elemental pure logical function is_digit(c)
      character, intent(in) :: c
      is_digit = scan(c, DIGIT_CHARACTERS) > 0
   end function is_digit

   ! Check if n is an integer >= 0
   pure logical function is_whole_number(n)
      integer, intent(in) :: n
      is_whole_number = .not. (n < 0)
   end function is_whole_number

   ! Convert c from digit character to integer
   pure integer function get_integer_digit(c)
      character, intent(in) :: c
      get_integer_digit = index(DIGIT_CHARACTERS, c) - 1
   end function get_integer_digit

   ! Convert index i character from s to integer
   pure integer function get_integer_digit_from_string(s, i)
      character(len=*), intent(in) :: s
      integer, intent(in) :: i

      if((i>0) .and. (i<len(s)+1)) then
         get_integer_digit_from_string = get_integer_digit(s(i:i))
      else
         get_integer_digit_from_string = INVALID
      end if
   end function get_integer_digit_from_string

   ! Convert string to whole number
   pure integer function read_whole_number(string)
      character(len=*), intent(in) :: string
      read_whole_number = read_whole_number_indexed(string, 1, len(string))
   end function read_whole_number

   ! Convert string(istart: istop) to whole number
   pure integer function read_whole_number_indexed(string, istart, istop)
      character(len=*), intent(in) :: string
      integer, intent(in) :: istart
      integer, intent(in) :: istop
      integer, parameter :: BASE=10
      integer :: n
      integer :: i
      integer :: place_value
      integer :: digit_value

      read_whole_number_indexed = INVALID

      ! Check indices
      if((istart < 1) .or. (istart > istop) .or. (istop > len(string))) return

      ! Convert characters from string, last to first, to integers,
      ! multiplies by place value, and adds
      place_value = 1
      n = 0
      do i= istop, istart, -1
         digit_value = get_integer_digit_from_string(string, i)
         if(is_whole_number(digit_value)) then
            n = n + digit_value * place_value
            place_value = place_value * BASE
         else
            n = INVALID
            exit
         end if
      end do
      read_whole_number_indexed = n
   end function read_whole_number_indexed

! END NUMBER HANDLING PROCEDURES


! LOW-LEVEL STRING PROCESSING PROCEDURES

   ! Strip delimiter from string
   pure function undelimit(string, delimiter) result(undelimited)
      character(len=*), intent(in) :: string
      character, intent(in) :: delimiter
      character(len=len(string)) :: undelimited
      integer :: i
      integer :: j

      undelimited = ''
      j = 0
      do i=1,len(string)
        if(string(i:i) == delimiter) cycle
        j = j + 1
        undelimited(j:j) = string(i:i)
      end do
   end function undelimit

! END LOW-LEVEL STRING PROCESSING PROCEDURES


! LOW-LEVEL DATE & TIME PROCESSING PROCEDURES

   ! Return true if y is a leap year, false otherwise
   pure logical function is_leap_year(y)
      integer, intent(in) :: y
      ! Leap years are years divisible by 400 or (years divisible by 4 and not divisible by 100)
      is_leap_year = (400 .divides. y) .or. ((4 .divides. y) .and. .not. (100 .divides. y))
   end function is_leap_year

   ! Return the last day numbers of each month based on the year
   pure function get_month_ends(y) result(month_ends)
      integer, intent(in) :: y
      integer, parameter :: NUM_MONTHS = 12
      integer, dimension(NUM_MONTHS) :: month_ends
      ! last day numbers of months for leap years
      integer, dimension(NUM_MONTHS) :: MONTH_END_LEAP
      ! last day numbers of months for regular years
      integer, dimension(NUM_MONTHS) :: MONTH_END
      MONTH_END = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
      MONTH_END_LEAP = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

      if(is_leap_year(y)) then
         month_ends = MONTH_END_LEAP
      else
         month_ends = MONTH_END
      endif
   end function get_month_ends

   ! Return the last day number of month m in year y
   pure integer function get_month_end(y, m)
      integer, intent(in) :: y
      integer, intent(in) :: m
      integer, dimension(:), allocatable :: month_ends

      month_ends = get_month_ends(y)
      get_month_end = month_ends(m)

   end function get_month_end

   ! Verify that y is a valid year according the ISO 8601 specification
   pure logical function is_valid_year(y)
      integer, intent(in) :: y
! Strict ISO8601 compliance does not allow years before 1583
#ifdef STRICT_ISO8601
      integer, parameter :: LB_YEAR = 1582
#else
      integer, parameter :: LB_YEAR = -1
#endif
      integer, parameter :: UB_YEAR = 10000

      is_valid_year = is_between(LB_YEAR, UB_YEAR, y)
   end function is_valid_year

   ! Verify that m is a valid month number
   pure logical function is_valid_month(m)
      integer, intent(in) :: m
      integer, parameter :: LB_MONTH = 0
      integer, parameter :: UB_MONTH = 13
      is_valid_month = is_between(LB_MONTH, UB_MONTH, m)
   end function is_valid_month

   ! Verify that hour is a valid hour
   pure logical function is_valid_hour(hour)
      integer, intent(in) :: hour
      integer, parameter :: LB_HOUR = -1
      integer, parameter :: UB_HOUR = 24
      is_valid_hour = is_between(LB_HOUR, UB_HOUR, hour)
   end function is_valid_hour

   ! Verify that minute is a valid minute
   pure logical function is_valid_minute(minute)
      integer, intent(in) :: minute
      integer, parameter :: LB_MINUTE = -1
      integer, parameter :: UB_MINUTE = 60
      is_valid_minute = is_between(LB_MINUTE, UB_MINUTE, minute)
   end function is_valid_minute

   ! Verify that second is a valid second
   ! ISO 8601 allows second=60 (for leap seconds)
   ! This function does not check if it is a valid leap second.
   pure logical function is_valid_second(second)
      integer, intent(in) :: second
      integer, parameter :: LB_SECOND = -1
      integer, parameter :: UB_SECOND = 61
      is_valid_second = is_between(LB_SECOND, UB_SECOND, second)
   end function is_valid_second

   ! Verify that millisecond is a valid millisecond values
   ! ISO 8601 allows fractional seconds, but it does not limit the fractional
   ! part to millisecond.
   pure logical function is_valid_millisecond(millisecond)
      integer, intent(in) :: millisecond
      integer, parameter :: LB_MILLISECOND = -1
      integer, parameter :: UB_MILLISECOND = 1000
      is_valid_millisecond = is_between(LB_MILLISECOND, UB_MILLISECOND, millisecond)
   end function is_valid_millisecond

   ! Verify that the timezone offset is valid
   ! Currently, the only valid offset is 0 corresponding to timezone Z.
   pure logical function is_valid_timezone_offset(timezone_offset)
      integer, intent(in) :: timezone_offset
      is_valid_timezone_offset = (timezone_offset == Z)
   end function is_valid_timezone_offset

! END LOW-LEVEL DATE & TIME PROCESSING PROCEDURES


! DATE & TIME VERIFICATION

   ! Verify all the date fields are valid
   pure logical function is_valid_date(date)
      type(date_fields), intent(in) :: date
      integer, parameter :: LB_DAY = 0

      is_valid_date = is_valid_year(date%year_) .and. &
         is_valid_month(date%month_) .and. &
         is_between(LB_DAY, get_month_end(date%year_, date%month_)+1, date%day_)

   end function is_valid_date

   ! Verify all the time fields are valid
   pure logical function is_valid_time(time)
      type(time_fields), intent(in) :: time

      is_valid_time = is_valid_hour(time%hour_) .and. &
         is_valid_minute(time%minute_) .and. &
         is_valid_second(time%second_) .and. &
         is_valid_millisecond(time%millisecond_) .and. &
         is_valid_timezone_offset(time%timezone_offset_)

   end function is_valid_time

! END DATE & TIME VERIFICATION


! STRING PARSERS

   ! parse string representing a timezone offset (absolute value)
   ! return integer offset in minutes, or on error return negative number
   pure function parse_timezone_offset(offset, field_width) result(tzo)
      character(len=*), intent(in) :: offset
      integer, intent(in) :: field_width
      integer :: offset_length
      integer :: tzo
      integer :: minutes
      integer :: hours

      offset_length = len_trim(offset)

      if(offset_length == field_width) then
         ! Offset with hours only
         hours = read_whole_number(offset)
         tzo = hours*MINUTES_PER_HOUR
      elseif (offset_length == 2*field_width) then
         ! Offset with hours and minutes
         hours = read_whole_number(offset(1:field_width))
         minutes = read_whole_number(offset(field_width+1:len(offset)))
         if(is_whole_number(hours) .and. is_whole_number(minutes)) then
            tzo = hours*MINUTES_PER_HOUR + minutes
         else
            tzo = INVALID
         end if
      else
         tzo = INVALID
      end if

   end function parse_timezone_offset

   ! Parse ISO 8601 Date string into date fields, check if valid date
   ! and set is_valid_ flag
   pure function parse_date(datestring) result(fields)
      character(len=*), intent(in) :: datestring
      type(date_fields) :: fields
      integer, parameter :: LENGTH = 8
      character, parameter :: DELIMITER = '-'
      integer, parameter :: YEAR_POSITION = 1
      integer, parameter :: MONTH_POSITION = 5
      integer, parameter :: DAY_POSITION = 7
      character(len=LENGTH) :: undelimited

      ! Eliminate delimiters so that there is one parsing block
      undelimited=undelimit(datestring, DELIMITER)

      if(len(undelimited) == LENGTH) then
         fields%year_ = read_whole_number(undelimited(YEAR_POSITION:MONTH_POSITION-1))
         fields%month_ = read_whole_number(undelimited(MONTH_POSITION:DAY_POSITION-1))
         fields%day_ = read_whole_number(undelimited(DAY_POSITION:LENGTH))
         fields%is_valid_ = is_valid_date(fields)
      else
         fields%is_valid_ = .FALSE.
      end if

   end function parse_date

   ! Parse ISO 8601 Time string into time fields, check if valid time
   ! and set is_valid_ flag
   pure function parse_time(timestring) result(fields)
      character(len=*), intent(in) :: timestring
      type(time_fields) :: fields
      integer, parameter :: LENGTH = 6
      character, parameter :: TIME_PREFIX = 'T' ! debug
      character, parameter :: DELIMITER = ':'
      character, parameter :: DECIMAL_POINT = '.'
      integer, parameter :: FIELDWIDTH = 2
      integer, parameter :: MS_WIDTH = 3
      integer, parameter :: PSTART = 1
      integer, parameter :: HSTART = 1
      integer, parameter :: HSTOP  = 2
      integer, parameter :: MSTART = 3
      integer, parameter :: MSTOP  = 4
      integer, parameter :: SSTART = 5
      integer, parameter :: SSTOP  = 6
      integer, parameter :: MS_START = 7
      integer, parameter :: MS_STOP  = 9
      logical :: has_millisecond
      integer :: pos
      character(len=len(timestring)) :: undelimited
      character :: c
      character(len=LENGTH) :: offset
      integer :: offset_minutes
      integer :: undelimited_length
      integer :: signum

      has_millisecond = .FALSE.
      offset_minutes = INVALID

      fields = time_fields(-1, -1, -1, -1, -1)

      ! Check for mandatory Time prefix
      pos = PSTART
      if(.not. timestring(pos:pos) == 'T') then
         fields%is_valid_ = .FALSE.
         return
      end if

      ! Find timezone portion
      pos = scan(timestring, '-Z+')

      if(.not. pos > 0) then
         fields%is_valid_ = .FALSE.
         return
      end if

      c = timestring(pos:pos)

      ! Check first character of timezone portion
      select case(c)
         case('Z')
            signum = 0
         case('-')
            signum = -1
         case('+')
            signum = +1
         case default
            fields%is_valid_ = .FALSE.
            return
      end select

      ! Set timezone offset
      if(signum == 0) then
         fields%timezone_offset_ = Z
         fields%is_valid_ = pos == len(timestring)
      else
         offset = undelimit(timestring(pos+1:len_trim(timestring)), DELIMITER)
         offset_minutes = parse_timezone_offset(offset, FIELDWIDTH)
         fields%timezone_offset_ = signum * offset_minutes
         fields%is_valid_ = is_whole_number(offset_minutes)
      end if

      if(.not. fields%is_valid_) return

      ! Select portion starting at fields%hour and ending before timezone
      undelimited = adjustl(timestring(PSTART+1:pos-1))

      ! Remove delimiter and decimal point
      undelimited=undelimit(undelimit(undelimited, DELIMITER), DECIMAL_POINT)
      undelimited_length = len_trim(undelimited)

      ! Check length of undelimited string with or without milliseconds
      if(undelimited_length == LENGTH) then
         fields%is_valid_ = .TRUE.
      elseif(undelimited_length == LENGTH+MS_WIDTH) then
         has_millisecond = .TRUE.
         fields%is_valid_ = .TRUE.
      else
         fields%is_valid_ = .FALSE.
      end if

      if(.not. fields%is_valid_) return

      ! Read time fields
      fields%hour_ = read_whole_number(undelimited(HSTART:HSTOP))
      fields%minute_ = read_whole_number(undelimited(MSTART:MSTOP))
      fields%second_ = read_whole_number(undelimited(SSTART:SSTOP))

      if(has_millisecond) then
         fields%millisecond_ = read_whole_number(undelimited(MS_START:MS_STOP))
      else
         fields%millisecond_ = 0
      end if

      fields%is_valid_ = is_valid_time(fields)
   end function parse_time

! END STRING PARSERS


! HIGH-LEVEL CONSTRUCTORS
   function construct_ISO8601Date(isostring, rc) result(date)
      character(len=*), intent(in) :: isostring
      integer, intent(out) :: rc
      type(ISO8601Date) :: date
      type(date_fields) :: fields
      fields = parse_date(trim(adjustl(isostring)))
      if(fields%is_valid_) then
         date%year_ = fields%year_
         date%month_ = fields%month_
         date%day_ = fields%day_
      else
         _FAIL('Invalid ISO 8601 date string')
      end if

      _RETURN(_SUCCESS)

   end function construct_ISO8601Date

   function construct_ISO8601Time(isostring, rc) result(time)
      character(len=*), intent(in) :: isostring
      integer, intent(inout) :: rc
      type(ISO8601Time) :: time
      type(time_fields) :: fields
      fields = parse_time(trim(adjustl(isostring)))
      if(fields%is_valid_) then
         time%hour_ = fields%hour_
         time%minute_ = fields%minute_
         time%second_ = fields%second_
         time%millisecond_ = fields%millisecond_
         time%timezone_offset_ = fields%timezone_offset_
         _RETURN(_SUCCESS)
      else
         _FAIL('Invalid ISO 8601 time string')
      end if

      _RETURN(_SUCCESS)

   end function construct_ISO8601Time

   function construct_ISO8601DateTime(isostring, rc) result(datetime)
      character(len=*), intent(in) :: isostring
      integer, optional, intent(inout) :: rc
      type(ISO8601DateTime) :: datetime
      character, parameter :: DELIMITER = TIME_PREFIX
      integer :: status
      integer :: time_index = 0
      time_index = index(isostring,TIME_PREFIX)
      if(time_index > 0) then
         datetime%date_ = ISO8601Date(isostring(1:time_index-1), _RC)
         datetime%time_ = ISO8601Time(isostring(time_index:len(isostring)), _RC)
         _RETURN(_SUCCESS)
      else
         _FAIL('Invalid ISO 8601 datetime string')
      end if
   end function construct_ISO8601DateTime

   ! This is not working currently.
   ! Construct ISO8601Duration from isostring from imin to imax
   function construct_ISO8601Duration(isostring, imin, imax, rc) result(duration)
      character(len=*), intent(in) :: isostring
      integer, intent(in) :: imin
      integer, intent(in) :: imax
      integer, optional, intent(out) :: rc
      type(ISO8601Duration) :: duration
      integer :: years = -1
      integer :: months = -1
      integer :: days = -1
      integer :: hours = -1
      integer :: minutes = -1
      integer :: seconds = -1
      integer :: istart = -1
      integer :: istop = -1
      logical :: time_found = .FALSE.
      logical :: successful = .FALSE.
      character :: c
      integer :: pos

      ! Check indices and first character is 'P'
      successful = ((imin > 0) .and. (imax <= len(isostring)) .and. &
         (imin <= imax) .and. (isostring(imin:imin) == 'P'))

      pos = imin + 1

      ! This do loop reads a character at a time, digit and nondigit.
      ! A field string consists of digits forming an integer n followed by
      ! a field character. A field character must be preceded by an integer.
      ! A field character indicates that the preceding digit characters
      ! should be processed as values for the corresponding field.
      ! The field characters are:
      !  Y(ear)
      !  M(onth)
      !  D(ay)
      !  H(our)
      !  M(inute)
      !  S(econd)
      ! The date and time portions of the string are delimited by T.
      ! Fields can be omitted, but they must be in order (see above).
      ! Omitted fields are set to 0.  A zero field cannot be specified by a
      ! field character without a preceding integer.
      do while(successful .and. (pos <= imax))
         successful = .FALSE.
         c = isostring(pos:pos)
         if(time_found) then
            ! Once the time is found, M should be processed as M(inute).
            select case(c)
               case('H')
                  ! Verify the field or preceding fields have not been set.
                  ! Then process the preceding digit character as an integer.
                  ! Once processed reset the istart index to start processing
                  ! digits. The same logic applies for each case below.
                  if(hours >= 0 .or. minutes >= 0 .or. &
                     seconds >= 0 .or. istart < 1) cycle
                  hours = read_whole_number_indexed(isostring, istart, istop)
                  if(hours < 0) cycle
                  istart = 0
               case('M')
                  if(minutes >= 0 .or. seconds >= 0 .or. istart < 1) cycle
                  minutes = read_whole_number_indexed(isostring, istart, istop)
                  if(minutes < 0) cycle
                  if(hours < 0) hours = 0
                  istart = 0
               case('S')
                  if(seconds >= 0 .or. istart < 1) cycle
                  seconds = read_whole_number_indexed(isostring, istart, istop)
                  if(seconds < 0) cycle
                  if(hours < 0) hours = 0
                  if(minutes < 0) minutes = 0
                  istart = 0
               case default
                  if(.not. is_digit(c)) cycle
                  if(istart == 0) istart = pos
                  istop = pos
            end select
         else
            ! Until the time is found, M should be processed as M(onth).
            select case(c)
               case('T')
                  time_found = .TRUE.
                  if(years < 0) years = 0
                  if(months < 0) months = 0
                  if(days < 0) days = 0
               case('Y')
                  if(years >= 0 .or. months >= 0 .or. &
                     days >= 0 .or. istart < 1) cycle
                  years = read_whole_number_indexed(isostring, istart, istop)
                  if(years < 0) cycle
                  istart = 0
               case('M')
                  if(months >= 0 .or. days >= 0 .or. istart < 1) cycle
                  months = read_whole_number_indexed(isostring, istart, istop)
                  if(months < 0) cycle
                  if(years < 0) years = 0
                  istart = 0
               case('D')
                  if(days >= 0 .or. istart < 1) cycle
                  days = read_whole_number_indexed(isostring, istart, istop)
                  if(days < 0) cycle
                  if(years < 0) years = 0
                  if(months < 0) months = 0
                  istart = 0
               case default
                  if(.not. is_digit(c)) cycle
                  ! istart == 0 indicates that a new integer is being processed.
                  if(istart == 0) istart = pos
                  ! The istop index should be the current position.
                  istop = pos
            end select
         end if
         successful = .TRUE.
         pos = pos + 1
      end do

      if(successful) then
         duration%years_= years
         duration%months_= months
         duration%days_= days
         duration%hours_= hours
         duration%minutes_= minutes
         duration%seconds_= seconds
         _RETURN(_SUCCESS)
      else
         _FAIL('Invalid ISO 8601 datetime duration string')
      end if
   end function construct_ISO8601Duration

! END HIGH-LEVEL CONSTRUCTORS


! LOW-LEVEL CONSTRUCTORS

   function construct_date_fields(year, month, day) result(fields)
      integer, intent(in) :: year
      integer, intent(in) :: month
      integer, intent(in) :: day
      type(date_fields) :: fields
      fields%year_ = year
      fields%month_ = month
      fields%day_ = day
   end function construct_date_fields

   pure function construct_time_fields(hour, minute, second, millisecond, &
      timezone_offset) result(fields)
      integer, intent(in) :: hour
      integer, intent(in) :: minute
      integer, intent(in) :: second
      integer, intent(in) :: millisecond
      integer, intent(in) :: timezone_offset
      type(time_fields) :: fields
      fields%hour_ = hour
      fields%minute_ = minute
      fields%second_ = second
      fields%millisecond_ = millisecond
      fields%timezone_offset_ = timezone_offset
   end function construct_time_fields

! END LOW-LEVEL CONSTRUCTORS


! TYPE-BOUND METHODS

! getters & setters

! ISO8601Date

   integer function get_year(self)
      class(ISO8601Date), intent(in) :: self
      get_year = self%year_
   end function get_year

   integer function get_month(self)
      class(ISO8601Date), intent(in) :: self
      get_month = self%month_
   end function get_month

   integer function get_day(self)
      class(ISO8601Date), intent(in) :: self
      get_day = self%day_
   end function get_day

! ISO8601Time
   integer function get_hour(self)
      class(ISO8601Time), intent(in) :: self
      get_hour = self%hour_
   end function get_hour

   integer function get_minute(self)
      class(ISO8601Time), intent(in) :: self
      get_minute = self%minute_
   end function get_minute

   integer function get_second(self)
      class(ISO8601Time), intent(in) :: self
      get_second = self%second_
   end function get_second

   integer function get_millisecond(self)
      class(ISO8601Time), intent(in) :: self
      get_millisecond = self%millisecond_
   end function get_millisecond

   integer function get_timezone_offset(self)
      class(ISO8601Time), intent(in) :: self
      get_timezone_offset = self%timezone_offset_
   end function get_timezone_offset

! ISO8601DateTime
   integer function get_year_datetime(self)
      class(ISO8601DateTime), intent(in) :: self
      get_year_datetime= self%date_%get_year()
   end function get_year_datetime

   integer function get_month_datetime(self)
      class(ISO8601DateTime), intent(in) :: self
      get_month_datetime = self%date_%get_month()
   end function get_month_datetime

   integer function get_day_datetime(self)
      class(ISO8601DateTime), intent(in) :: self
      get_day_datetime = self%date_%get_day()
   end function get_day_datetime

   integer function get_hour_datetime(self)
      class(ISO8601DateTime), intent(in) :: self
      get_hour_datetime = self%time_%get_hour()
   end function get_hour_datetime

   integer function get_minute_datetime(self)
      class(ISO8601DateTime), intent(in) :: self
      get_minute_datetime = self%time_%get_minute()
   end function get_minute_datetime

   integer function get_second_datetime(self)
      class(ISO8601DateTime), intent(in) :: self
      get_second_datetime = self%time_%get_second()
   end function get_second_datetime

   integer function get_millisecond_datetime(self)
      class(ISO8601DateTime), intent(in) :: self
      get_millisecond_datetime = self%time_%get_millisecond()
   end function get_millisecond_datetime

   integer function get_timezone_offset_datetime(self)
      class(ISO8601DateTime), intent(in) :: self
      get_timezone_offset_datetime = self%time_%get_timezone_offset()
   end function get_timezone_offset_datetime

   function get_date(self) result(date)
      class(ISO8601DateTime), intent(in) :: self
      type(ISO8601Date) :: date
      date = self%date_
   end function get_date

   function get_time(self) result(time)
      class(ISO8601DateTime), intent(in) :: self
      type(ISO8601Time) :: time
      time = self%time_
   end function get_time

! ISO8601Duration
   integer function get_years(self)
      class(ISO8601Duration), intent(in) :: self
      get_years = self%years_
   end function get_years

   integer function get_months(self)
      class(ISO8601Duration), intent(in) :: self
      get_months = self%months_
   end function get_months

   integer function get_days(self)
      class(ISO8601Duration), intent(in) :: self
      get_days = self%days_
   end function get_days

   integer function get_hours(self)
      class(ISO8601Duration), intent(in) :: self
      get_hours = self%hours_
   end function get_hours

   integer function get_minutes(self)
      class(ISO8601Duration), intent(in) :: self
      get_minutes = self%minutes_
   end function get_minutes

   integer function get_seconds(self)
      class(ISO8601Duration), intent(in) :: self
      get_seconds = self%seconds_
   end function get_seconds

! ISO8601Interval
   function get_start_datetime(self) result(datetime)
      class(ISO8601Interval), intent(in) :: self
      type(ISO8601DateTime) :: datetime
      datetime = self%start_datetime_
   end function get_start_datetime

   function get_end_datetime(self) result(datetime)
      class(ISO8601Interval), intent(in) :: self
      type(ISO8601DateTime) :: datetime
      datetime = self%end_datetime_
   end function get_end_datetime

   integer function get_repetitions(self)
      class(ISO8601Interval), intent(in) :: self
      get_repetitions = self%repetitions_
   end function get_repetitions

! END TYPE-BOUND METHODS


! HIGH-LEVEL CONVERSION PROCEDURES

   ! Convert ISO 8601 string to packed integer YYYYMMDD
   function convert_ISO8601_to_integer_date(isostring, rc) result(integer_date)
      character(len=*), intent(in) :: isostring
      integer, optional, intent(out) :: rc
      integer :: integer_date
      type(ISO8601Date) :: date
      integer :: status

      date = ISO8601Date(isostring, _RC)

      integer_date = date%get_year()*ID_YEAR + date%get_month()*ID_MONTH + &
         date%get_day()*ID_DAY

      _RETURN(_SUCCESS)
   end function convert_ISO8601_to_integer_date

   ! Convert ISO 8601 string to packed integer HHMMSS
   function convert_ISO8601_to_integer_time(isostring, rc) result(integer_time)
      character(len=*), intent(in) :: isostring
      integer, optional, intent(out) :: rc
      integer :: integer_time
      type(ISO8601Time) :: time
      integer :: status

      time = ISO8601Time(isostring, _RC)

      integer_time = time%get_hour()*IT_HOUR + time%get_minute()*IT_MINUTE + &
         time%get_second()*IT_SECOND

      _RETURN(_SUCCESS)
   end function convert_ISO8601_to_integer_time

! END HIGH-LEVEL CONVERSION PROCEDURES

end module MAPL_ISO8601_DateTime