stdlib_datetime.f90 Source File


Source Code

module stdlib_datetime
    !! version: experimental
    !!
    !! Date, time, and time interval handling for Fortran.
    !! ([Specification](../page/specs/stdlib_datetime.html))
    use stdlib_kinds, only: dp, int64
    use stdlib_strings, only: to_string
    implicit none
    private

    public :: datetime_type, timedelta_type
    public :: datetime, timedelta, now, now_utc, epoch
    public :: parse_datetime, format_datetime, format_timedelta
    public :: is_leap_year, days_in_month, days_in_year
    public :: day_of_year, day_of_week, to_utc, total_seconds
    public :: operator(+), operator(-)
    public :: operator(==), operator(/=)
    public :: operator(<), operator(<=)
    public :: operator(>), operator(>=)

    type :: datetime_type
        !! version: experimental
        !!
        !! Represents a specific point in time.
        integer :: year        = 1   !! Year (1-9999)
        integer :: month       = 1   !! Month (1-12)
        integer :: day         = 1   !! Day (1-31)
        integer :: hour        = 0   !! Hour (0-23)
        integer :: minute      = 0   !! Minute (0-59)
        integer :: second      = 0   !! Second (0-59)
        integer :: millisecond = 0   !! Millisecond (0-999)
        integer :: utc_offset_minutes = 0 !! UTC offset in minutes
    end type datetime_type

    type :: timedelta_type
        !! version: experimental
        !!
        !! Represents a duration or time interval.
        !! Normalized: seconds in [0,86399], ms in [0,999].
        !! Days can be negative for negative durations.
        integer :: days         = 0   !! Number of days
        integer :: seconds      = 0   !! Seconds (0-86399)
        integer :: milliseconds = 0   !! Milliseconds (0-999)
    end type timedelta_type

    integer(int64), parameter :: MS_PER_SEC  = 1000_int64
    integer(int64), parameter :: MS_PER_MIN  = 60000_int64
    integer(int64), parameter :: MS_PER_HOUR = 3600000_int64
    integer(int64), parameter :: MS_PER_DAY  = 86400000_int64

    interface operator(+)
        module procedure dt_plus_td
        module procedure td_plus_dt
        module procedure td_plus_td
    end interface

    interface operator(-)
        module procedure dt_minus_td
        module procedure dt_minus_dt
        module procedure td_minus_td
        module procedure td_negate
    end interface

    interface operator(==)
        module procedure dt_eq
        module procedure td_eq
    end interface

    interface operator(/=)
        module procedure dt_ne
        module procedure td_ne
    end interface

    interface operator(<)
        module procedure dt_lt
        module procedure td_lt
    end interface

    interface operator(<=)
        module procedure dt_le
        module procedure td_le
    end interface

    interface operator(>)
        module procedure dt_gt
        module procedure td_gt
    end interface

    interface operator(>=)
        module procedure dt_ge
        module procedure td_ge
    end interface

    interface is_leap_year
        module procedure is_leap_year_int
        module procedure is_leap_year_dt
    end interface

contains

    pure function datetime(year, month, day, hour, minute, &
                           second, millisecond, &
                           utc_offset_minutes) result(dt)
        !! version: experimental
        !!
        !! Create a datetime_type from individual components.
        integer, intent(in), optional :: year, month, day
        integer, intent(in), optional :: hour, minute, second
        integer, intent(in), optional :: millisecond
        integer, intent(in), optional :: utc_offset_minutes
        type(datetime_type) :: dt
        if (present(year))        dt%year        = year
        if (present(month))       dt%month       = month
        if (present(day))         dt%day         = day
        if (present(hour))        dt%hour        = hour
        if (present(minute))      dt%minute      = minute
        if (present(second))      dt%second      = second
        if (present(millisecond)) dt%millisecond = millisecond
        if (present(utc_offset_minutes)) &
            dt%utc_offset_minutes = utc_offset_minutes
    end function datetime

    pure function timedelta(days, hours, minutes, seconds, &
                            milliseconds) result(td)
        !! version: experimental
        !!
        !! Create a normalized timedelta_type from mixed units.
        integer, intent(in), optional :: days, hours, minutes
        integer, intent(in), optional :: seconds, milliseconds
        type(timedelta_type) :: td
        integer(int64) :: total_ms
        total_ms = 0_int64
        if (present(days)) &
            total_ms = total_ms &
                     + int(days, int64) * MS_PER_DAY
        if (present(hours)) &
            total_ms = total_ms &
                     + int(hours, int64) * MS_PER_HOUR
        if (present(minutes)) &
            total_ms = total_ms &
                     + int(minutes, int64) * MS_PER_MIN
        if (present(seconds)) &
            total_ms = total_ms &
                     + int(seconds, int64) * MS_PER_SEC
        if (present(milliseconds)) &
            total_ms = total_ms + int(milliseconds, int64)
        td = ms_to_td(total_ms)
    end function timedelta

    function now() result(dt)
        !! version: experimental
        !!
        !! Return the current local date and time.
        type(datetime_type) :: dt
        integer :: v(8)
        call date_and_time(values=v)
        dt = datetime_type(v(1), v(2), v(3), v(5), &
                           v(6), v(7), v(8), v(4))
    end function now

    function now_utc() result(dt)
        !! version: experimental
        !!
        !! Return the current UTC date and time.
        type(datetime_type) :: dt
        dt = to_utc(now())
    end function now_utc

    pure function epoch() result(dt)
        !! version: experimental
        !!
        !! Return the Unix epoch: 1970-01-01T00:00:00Z.
        type(datetime_type) :: dt
        dt = datetime_type(1970, 1, 1, 0, 0, 0, 0, 0)
    end function epoch

    pure function dt_plus_td(dt, td) result(res)
        !! datetime + timedelta
        type(datetime_type), intent(in)  :: dt
        type(timedelta_type), intent(in) :: td
        type(datetime_type) :: res
        res = epoch_ms_to_dt( &
            dt_to_epoch_ms(dt) + td_to_ms(td), &
            dt%utc_offset_minutes)
    end function dt_plus_td

    pure function td_plus_dt(td, dt) result(res)
        !! timedelta + datetime (commutative)
        type(timedelta_type), intent(in) :: td
        type(datetime_type), intent(in)  :: dt
        type(datetime_type) :: res
        res = dt_plus_td(dt, td)
    end function td_plus_dt

    pure function td_plus_td(td1, td2) result(res)
        !! timedelta + timedelta
        type(timedelta_type), intent(in) :: td1, td2
        type(timedelta_type) :: res
        res = ms_to_td(td_to_ms(td1) + td_to_ms(td2))
    end function td_plus_td

    pure function dt_minus_td(dt, td) result(res)
        !! datetime - timedelta
        type(datetime_type), intent(in)  :: dt
        type(timedelta_type), intent(in) :: td
        type(datetime_type) :: res
        res = epoch_ms_to_dt( &
            dt_to_epoch_ms(dt) - td_to_ms(td), &
            dt%utc_offset_minutes)
    end function dt_minus_td

    pure function dt_minus_dt(dt1, dt2) result(res)
        !! datetime - datetime (both converted to UTC)
        type(datetime_type), intent(in) :: dt1, dt2
        type(timedelta_type) :: res
        res = ms_to_td(dt_to_utc_ms(dt1) - dt_to_utc_ms(dt2))
    end function dt_minus_dt

    pure function td_minus_td(td1, td2) result(res)
        !! timedelta - timedelta
        type(timedelta_type), intent(in) :: td1, td2
        type(timedelta_type) :: res
        res = ms_to_td(td_to_ms(td1) - td_to_ms(td2))
    end function td_minus_td

    pure function td_negate(td) result(res)
        !! Unary minus: -timedelta
        type(timedelta_type), intent(in) :: td
        type(timedelta_type) :: res
        res = ms_to_td(-td_to_ms(td))
    end function td_negate

    pure function dt_eq(dt1, dt2) result(res)
        type(datetime_type), intent(in) :: dt1, dt2
        logical :: res
        res = dt_to_utc_ms(dt1) == dt_to_utc_ms(dt2)
    end function dt_eq

    pure function dt_ne(dt1, dt2) result(res)
        type(datetime_type), intent(in) :: dt1, dt2
        logical :: res
        res = dt_to_utc_ms(dt1) /= dt_to_utc_ms(dt2)
    end function dt_ne

    pure function dt_lt(dt1, dt2) result(res)
        type(datetime_type), intent(in) :: dt1, dt2
        logical :: res
        res = dt_to_utc_ms(dt1) < dt_to_utc_ms(dt2)
    end function dt_lt

    pure function dt_le(dt1, dt2) result(res)
        type(datetime_type), intent(in) :: dt1, dt2
        logical :: res
        res = dt_to_utc_ms(dt1) <= dt_to_utc_ms(dt2)
    end function dt_le

    pure function dt_gt(dt1, dt2) result(res)
        type(datetime_type), intent(in) :: dt1, dt2
        logical :: res
        res = dt_to_utc_ms(dt1) > dt_to_utc_ms(dt2)
    end function dt_gt

    pure function dt_ge(dt1, dt2) result(res)
        type(datetime_type), intent(in) :: dt1, dt2
        logical :: res
        res = dt_to_utc_ms(dt1) >= dt_to_utc_ms(dt2)
    end function dt_ge

    pure function td_eq(td1, td2) result(res)
        type(timedelta_type), intent(in) :: td1, td2
        logical :: res
        res = td_to_ms(td1) == td_to_ms(td2)
    end function td_eq

    pure function td_ne(td1, td2) result(res)
        type(timedelta_type), intent(in) :: td1, td2
        logical :: res
        res = td_to_ms(td1) /= td_to_ms(td2)
    end function td_ne

    pure function td_lt(td1, td2) result(res)
        type(timedelta_type), intent(in) :: td1, td2
        logical :: res
        res = td_to_ms(td1) < td_to_ms(td2)
    end function td_lt

    pure function td_le(td1, td2) result(res)
        type(timedelta_type), intent(in) :: td1, td2
        logical :: res
        res = td_to_ms(td1) <= td_to_ms(td2)
    end function td_le

    pure function td_gt(td1, td2) result(res)
        type(timedelta_type), intent(in) :: td1, td2
        logical :: res
        res = td_to_ms(td1) > td_to_ms(td2)
    end function td_gt

    pure function td_ge(td1, td2) result(res)
        type(timedelta_type), intent(in) :: td1, td2
        logical :: res
        res = td_to_ms(td1) >= td_to_ms(td2)
    end function td_ge

    pure function format_datetime(dt) result(str)
        !! version: experimental
        !!
        !! Format a datetime_type as an ISO 8601 string.
        type(datetime_type), intent(in) :: dt
        character(:), allocatable :: str
        integer :: off_h, off_m

        str = to_string(dt%year, '(I4.4)') // '-' // &
              to_string(dt%month, '(I2.2)') // '-' // &
              to_string(dt%day, '(I2.2)') // 'T' // &
              to_string(dt%hour, '(I2.2)') // ':' // &
              to_string(dt%minute, '(I2.2)') // ':' // &
              to_string(dt%second, '(I2.2)')

        if (dt%millisecond /= 0) then
            str = str // '.' // to_string(dt%millisecond, '(I3.3)')
        end if

        if (dt%utc_offset_minutes == 0) then
            str = str // 'Z'
        else
            off_h = abs(dt%utc_offset_minutes) / 60
            off_m = mod(abs(dt%utc_offset_minutes), 60)
            if (dt%utc_offset_minutes > 0) then
                str = str // '+'
            else
                str = str // '-'
            end if
            str = str // to_string(off_h, '(I2.2)') // ':' // &
                  to_string(off_m, '(I2.2)')
        end if
    end function format_datetime

    pure function format_timedelta(td) result(str)
        !! version: experimental
        !!
        !! Format a timedelta_type as a readable string.
        type(timedelta_type), intent(in) :: td
        character(:), allocatable :: str
        integer :: h, m, s

        h = td%seconds / 3600
        m = mod(td%seconds, 3600) / 60
        s = mod(td%seconds, 60)

        str = to_string(td%days, '(I0)') // ' days, ' // &
              to_string(h, '(I2.2)') // ':' // &
              to_string(m, '(I2.2)') // ':' // &
              to_string(s, '(I2.2)')

        if (td%milliseconds /= 0) then
            str = str // '.' // to_string(td%milliseconds, '(I3.3)')
        end if
    end function format_timedelta

    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

    pure elemental function is_leap_year_int(year) &
        result(res)
        !! version: experimental
        !!
        !! Check if a year is a leap year.
        integer, intent(in) :: year
        logical :: res
        res = (mod(year, 4) == 0 &
               .and. mod(year, 100) /= 0) &
              .or. (mod(year, 400) == 0)
    end function is_leap_year_int

    pure elemental function is_leap_year_dt(dt) &
        result(res)
        !! version: experimental
        !!
        !! Check if a datetime's year is a leap year.
        type(datetime_type), intent(in) :: dt
        logical :: res
        res = is_leap_year_int(dt%year)
    end function is_leap_year_dt

    pure function days_in_month(month, year) result(d)
        !! version: experimental
        !!
        !! Return the number of days in a given month.
        integer, intent(in) :: month, year
        integer :: d
        integer, parameter :: mdays(12) = &
            [31,28,31,30,31,30,31,31,30,31,30,31]
        if (month < 1 .or. month > 12) then
            d = 0
            return
        end if
        d = mdays(month)
        if (month == 2 .and. is_leap_year_int(year)) &
            d = 29
    end function days_in_month

    pure function days_in_year(year) result(d)
        !! version: experimental
        !!
        !! Return 366 for leap years, 365 otherwise.
        integer, intent(in) :: year
        integer :: d
        d = merge(366, 365, is_leap_year_int(year))
    end function days_in_year

    pure function day_of_year(dt) result(doy)
        !! version: experimental
        !!
        !! Return the ordinal day of the year (1-366).
        type(datetime_type), intent(in) :: dt
        integer :: doy
        integer, parameter :: cum(12) = &
            [0,31,59,90,120,151,181,212,243,273,304,334]
        ! Guard against invalid month values
        if (dt%month < 1 .or. dt%month > 12) then
            doy = 0
            return
        end if
        doy = cum(dt%month) + dt%day
        if (dt%month > 2 .and. is_leap_year_int(dt%year))&
            doy = doy + 1
    end function day_of_year

    pure function day_of_week(dt) result(dow)
        !! version: experimental
        !!
        !! Return ISO weekday (1=Monday ... 7=Sunday).
        type(datetime_type), intent(in) :: dt
        integer :: dow
        integer :: y, w
        integer, parameter :: t(12) = &
            [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]
        ! Guard against invalid month values
        if (dt%month < 1 .or. dt%month > 12) then
            dow = 0
            return
        end if
        y = dt%year
        if (dt%month < 3) y = y - 1
        w = mod(y + y/4 - y/100 + y/400 &
                + t(dt%month) + dt%day, 7)
        dow = mod(w + 6, 7) + 1
    end function day_of_week

    pure function to_utc(dt) result(utc_dt)
        !! version: experimental
        !!
        !! Convert a datetime to UTC.
        type(datetime_type), intent(in) :: dt
        type(datetime_type) :: utc_dt
        utc_dt = epoch_ms_to_dt(dt_to_utc_ms(dt), 0)
    end function to_utc

    pure function total_seconds(td) result(secs)
        !! version: experimental
        !!
        !! Return the total duration in seconds as real(dp).
        type(timedelta_type), intent(in) :: td
        real(dp) :: secs
        secs = real(td%days, dp) * 86400.0_dp &
             + real(td%seconds, dp) &
             + real(td%milliseconds, dp) * 0.001_dp
    end function total_seconds

    pure function days_from_civil(y, m, d) result(days)
        !! Convert civil date to days since 1970-01-01.
        !! Howard Hinnant's algorithm (public domain).
        integer, intent(in) :: y, m, d
        integer(int64) :: days
        integer :: yr, era, yoe, doy, doe, mp
        yr = y
        if (m <= 2) yr = yr - 1
        if (yr >= 0) then
            era = yr / 400
        else
            era = (yr - 399) / 400
        end if
        yoe = yr - era * 400
        if (m > 2) then
            mp = m - 3
        else
            mp = m + 9
        end if
        doy = (153 * mp + 2) / 5 + d - 1
        doe = yoe * 365 + yoe/4 - yoe/100 + doy
        days = int(era, int64) * 146097_int64 &
             + int(doe, int64) - 719468_int64
    end function days_from_civil

    pure subroutine civil_from_days(z, y, m, d)
        !! Convert days since 1970-01-01 to civil date.
        !! Howard Hinnant's algorithm (public domain).
        integer(int64), intent(in) :: z
        integer, intent(out) :: y, m, d
        integer(int64) :: zz, era64
        integer :: doe, yoe, doy, mp, era
        zz = z + 719468_int64
        if (zz >= 0) then
            era64 = zz / 146097_int64
        else
            era64 = (zz - 146096_int64) / 146097_int64
        end if
        era = int(era64)
        doe = int(zz - era64 * 146097_int64)
        yoe = (doe - doe/1461 + doe/36524 &
               - doe/146096) / 365
        y = yoe + era * 400
        doy = doe - (365*yoe + yoe/4 - yoe/100)
        mp = (5*doy + 2) / 153
        d = doy - (153*mp + 2)/5 + 1
        if (mp < 10) then
            m = mp + 3
        else
            m = mp - 9
        end if
        if (m <= 2) y = y + 1
    end subroutine civil_from_days

    pure function dt_to_epoch_ms(dt) result(ms)
        !! Datetime to milliseconds since epoch (local).
        type(datetime_type), intent(in) :: dt
        integer(int64) :: ms
        ms = days_from_civil(dt%year, dt%month, dt%day) &
                * MS_PER_DAY &
           + int(dt%hour, int64)        * MS_PER_HOUR &
           + int(dt%minute, int64)      * MS_PER_MIN &
           + int(dt%second, int64)      * MS_PER_SEC &
           + int(dt%millisecond, int64)
    end function dt_to_epoch_ms

    pure function dt_to_utc_ms(dt) result(ms)
        !! Datetime to UTC milliseconds since epoch.
        type(datetime_type), intent(in) :: dt
        integer(int64) :: ms
        ms = dt_to_epoch_ms(dt) &
           - int(dt%utc_offset_minutes, int64) * MS_PER_MIN
    end function dt_to_utc_ms

    pure function epoch_ms_to_dt(ms, utc_offset) result(dt)
        !! Milliseconds since epoch to datetime.
        integer(int64), intent(in) :: ms
        integer, intent(in) :: utc_offset
        type(datetime_type) :: dt
        integer(int64) :: d, rem
        d = ms / MS_PER_DAY
        rem = ms - d * MS_PER_DAY
        if (rem < 0) then
            d = d - 1_int64
            rem = rem + MS_PER_DAY
        end if
        call civil_from_days(d, dt%year, dt%month, dt%day)
        dt%hour        = int(rem / MS_PER_HOUR)
        rem = mod(rem, MS_PER_HOUR)
        dt%minute      = int(rem / MS_PER_MIN)
        rem = mod(rem, MS_PER_MIN)
        dt%second      = int(rem / MS_PER_SEC)
        dt%millisecond = int(mod(rem, MS_PER_SEC))
        dt%utc_offset_minutes = utc_offset
    end function epoch_ms_to_dt

    pure function td_to_ms(td) result(ms)
        !! Timedelta to total milliseconds.
        type(timedelta_type), intent(in) :: td
        integer(int64) :: ms
        ms = int(td%days, int64)         * MS_PER_DAY &
           + int(td%seconds, int64)      * MS_PER_SEC &
           + int(td%milliseconds, int64)
    end function td_to_ms

    pure function ms_to_td(ms) result(td)
        !! Total milliseconds to normalized timedelta.
        integer(int64), intent(in) :: ms
        type(timedelta_type) :: td
        integer(int64) :: rem
        td%days = int(ms / MS_PER_DAY)
        rem = ms - int(td%days, int64) * MS_PER_DAY
        if (rem < 0) then
            td%days = td%days - 1
            rem = rem + MS_PER_DAY
        end if
        td%seconds      = int(rem / MS_PER_SEC)
        td%milliseconds = int(mod(rem, MS_PER_SEC))
    end function ms_to_td

end module stdlib_datetime