parse_datetime Function

public function parse_datetime(str, stat) result(dt)

Parse an ISO 8601 date/time string.

Arguments

Type IntentOptional Attributes Name
character(len=*), intent(in) :: str
integer, intent(out), optional :: stat

Return Value type(datetime_type)


Source Code

    function parse_datetime(str, stat) result(dt)
        !! version: experimental
        !!
        !! Parse an ISO 8601 date/time string.
        character(len=*), intent(in) :: str
        integer, intent(out), optional :: stat
        type(datetime_type) :: dt
        integer :: slen, ios, off_h, off_m, ms_end
        integer :: max_day
        character(len=1) :: sign_ch
        character(len=32) :: tmp_str
        real(dp) :: ms_frac

        if (present(stat)) stat = 0
        dt = datetime_type()
        slen = len_trim(str)

        ! Require at least YYYY-MM-DD (10 characters)
        if (slen < 10) then
            if (present(stat)) stat = 1
            return
        end if

        ! Check required date separators for ISO 8601 (YYYY-MM-DD)
        if (str(5:5) /= '-' .or. str(8:8) /= '-') then
            if (present(stat)) stat = 1
            return
        end if

        read(str(1:4), '(I4)', iostat=ios) dt%year
        if (ios /= 0) then
            if (present(stat)) stat = 1
            return
        end if
        read(str(6:7), '(I2)', iostat=ios) dt%month
        if (ios /= 0) then
            if (present(stat)) stat = 1
            return
        end if
        ! Validate month range [1,12]
        if (dt%month < 1 .or. dt%month > 12) then
            if (present(stat)) stat = 1
            return
        end if
        read(str(9:10), '(I2)', iostat=ios) dt%day
        if (ios /= 0) then
            if (present(stat)) stat = 1
            return
        end if
        ! Validate day range [1, days_in_month]
        max_day = days_in_month(dt%month, dt%year)
        if (dt%day < 1 .or. dt%day > max_day) then
            if (present(stat)) stat = 1
            return
        end if

        if (slen == 10) return

        if (str(11:11) /= 'T' .and. &
            str(11:11) /= 't' .and. &
            str(11:11) /= ' ') then
            if (present(stat)) stat = 1
            return
        end if

        if (slen < 19) then
            if (present(stat)) stat = 1
            return
        end if

        ! Validate required time separators (HH:MM:SS)
        if (str(14:14) /= ':' .or. str(17:17) /= ':') then
            if (present(stat)) stat = 1
            return
        end if

        read(str(12:13), '(I2)', iostat=ios) dt%hour
        if (ios /= 0) then
            if (present(stat)) stat = 1
            return
        end if
        ! Validate hour range [0,23]
        if (dt%hour < 0 .or. dt%hour > 23) then
            if (present(stat)) stat = 1
            return
        end if
        read(str(15:16), '(I2)', iostat=ios) dt%minute
        if (ios /= 0) then
            if (present(stat)) stat = 1
            return
        end if
        ! Validate minute range [0,59]
        if (dt%minute < 0 .or. dt%minute > 59) then
            if (present(stat)) stat = 1
            return
        end if
        read(str(18:19), '(I2)', iostat=ios) dt%second
        if (ios /= 0) then
            if (present(stat)) stat = 1
            return
        end if
        ! Validate second range [0,59]
        if (dt%second < 0 .or. dt%second > 59) then
            if (present(stat)) stat = 1
            return
        end if

        if (slen == 19) return

        ms_end = 19
        if (str(20:20) == '.') then
            ms_end = 20
            do while (ms_end < slen)
                sign_ch = str(ms_end+1:ms_end+1)
                if (sign_ch >= '0' .and. sign_ch <= '9') then
                    ms_end = ms_end + 1
                else
                    exit
                end if
            end do
            if (ms_end == 20) then
                ! "." without following digits
                if (present(stat)) stat = 1
                return
            end if
            tmp_str = '0' // str(20:ms_end)
            read(tmp_str, *, iostat=ios) ms_frac
            if (ios /= 0) then
                if (present(stat)) stat = 1
                return
            end if
            dt%millisecond = nint(ms_frac * 1000.0_dp)
        end if

        if (slen <= ms_end) return

        sign_ch = str(ms_end+1:ms_end+1)
        if (sign_ch == 'Z' .or. sign_ch == 'z') then
            dt%utc_offset_minutes = 0
        else if (sign_ch == '+' .or. sign_ch == '-') then
            if (slen < ms_end + 6) then
                if (present(stat)) stat = 1
                return
            end if
            read(str(ms_end+2:ms_end+3), '(I2)', &
                 iostat=ios) off_h
            if (ios /= 0) then
                if (present(stat)) stat = 1
                return
            end if
            ! Require ':' between offset hours and minutes
            if (str(ms_end+4:ms_end+4) /= ':') then
                if (present(stat)) stat = 1
                return
            end if
            read(str(ms_end+5:ms_end+6), '(I2)', &
                 iostat=ios) off_m
            if (ios /= 0) then
                if (present(stat)) stat = 1
                return
            end if
            ! Validate timezone offset ranges
            if (off_h < 0 .or. off_h > 23 .or. &
                off_m < 0 .or. off_m > 59) then
                if (present(stat)) stat = 1
                return
            end if
            dt%utc_offset_minutes = off_h * 60 + off_m
            if (sign_ch == '-') &
                dt%utc_offset_minutes = &
                    -dt%utc_offset_minutes
        else
            if (present(stat)) stat = 1
            return
        end if
    end function parse_datetime