Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion doc/specs/stdlib_datetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,14 +243,22 @@ Pure function.
#### Description

Formats a `timedelta_type` as a human-readable string.
For zero-day durations, compact forms are used:
- `5ms` for sub-second values
- `1.500s` or `42s` for sub-minute values
- `MM:SS` when hours are zero
- `HH:MM:SS[.mmm]` otherwise

For nonzero-day durations, the verbose form is preserved:
`X days, HH:MM:SS[.mmm]`.

#### Syntax

`str = ` [[stdlib_datetime(module):format_timedelta(function)]] `(td)`

#### Return value

`character(:), allocatable` — e.g. `"30 days, 01:30:00"`.
`character(:), allocatable` — e.g. `"5ms"`, `"1.500s"`, `"05:30"`, `"01:02:03.456"`, or `"30 days, 01:30:00"`.

## Utility Functions

Expand Down
14 changes: 13 additions & 1 deletion example/datetime/example_datetime_usage.f90
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,19 @@ program example_datetime
t3 = datetime_type(2026, 3, 17, 17, 30, 0, 0, 330)
print '(A,L1)', '12:00Z == 17:30+05:30? ', t2 == t3

! 9. Unix epoch
! 9. Compact format_timedelta (sub-second / sub-minute)
print *
print '(A)', '=== Compact Duration Formatting ==='
duration = timedelta(milliseconds=5)
print '(A,T40,A)', 'timedelta(ms=5):', format_timedelta(duration)
duration = timedelta(milliseconds=1500)
print '(A,T40,A)', 'timedelta(ms=1500):', format_timedelta(duration)
duration = timedelta(seconds=65)
print '(A,T40,A)', 'timedelta(s=65):', format_timedelta(duration)
duration = timedelta(hours=1, minutes=2, seconds=3)
print '(A,T40,A)', 'timedelta(1h 2m 3s):', format_timedelta(duration)

! 10. Unix epoch
print *
print '(A,A)', 'Unix epoch: ', &
format_datetime(epoch())
Expand Down
58 changes: 48 additions & 10 deletions src/datetime/stdlib_datetime.f90
Original file line number Diff line number Diff line change
Expand Up @@ -340,21 +340,59 @@ pure function format_timedelta(td) result(str)
!! version: experimental
!!
!! Format a timedelta_type as a readable string.
!! When days == 0, sub-second/sub-minute durations are formatted
!! compactly (e.g. "5ms", "1.500s", "05:30"). The "0 days, "
!! prefix is omitted. When days /= 0, original verbose format
!! is preserved.
type(timedelta_type), intent(in) :: td
character(:), allocatable :: str
integer :: h, m, s
integer :: h, m, s, ms

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

if (td%days == 0) then
! Compact formatting for zero-day durations
if (h == 0 .and. m == 0 .and. s == 0) then
if (ms == 0) then
str = "0s"
else
str = to_string(ms, '(I0)') // "ms"
end if
return
end if

h = td%seconds / 3600
m = mod(td%seconds, 3600) / 60
s = mod(td%seconds, 60)
if (h == 0 .and. m == 0) then
if (ms == 0) then
str = to_string(s, '(I0)') // "s"
else
str = to_string(s, '(I0)') // "." // to_string(ms, '(I3.3)') // "s"
end if
return
end if

str = to_string(td%days, '(I0)') // ' days, ' // &
to_string(h, '(I2.2)') // ':' // &
to_string(m, '(I2.2)') // ':' // &
to_string(s, '(I2.2)')
if (h > 0) then
str = to_string(h, '(I2.2)') // ":" // &
to_string(m, '(I2.2)') // ":" // &
to_string(s, '(I2.2)')
else
str = to_string(m, '(I2.2)') // ":" // &
to_string(s, '(I2.2)')
end if

if (td%milliseconds /= 0) then
str = str // '.' // to_string(td%milliseconds, '(I3.3)')
if (ms /= 0) str = str // "." // to_string(ms, '(I3.3)')
else
! Original format for durations with days
str = to_string(td%days, '(I0)') // ' days, ' // &
to_string(h, '(I2.2)') // ':' // &
to_string(m, '(I2.2)') // ':' // &
to_string(s, '(I2.2)')

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

Expand Down
88 changes: 87 additions & 1 deletion test/datetime/test_datetime.f90
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ subroutine collect_datetime(testsuite)
test_format_datetime_offset), &
new_unittest("format_timedelta_test", &
test_format_timedelta), &
new_unittest("format_timedelta_compact", &
test_format_timedelta_compact), &
new_unittest("timedelta_ms_rollover", &
test_timedelta_ms_rollover), &
new_unittest("to_utc_test", &
Expand Down Expand Up @@ -505,13 +507,97 @@ end subroutine test_format_datetime_offset
subroutine test_format_timedelta(error)
type(error_type), allocatable, intent(out) :: error
type(timedelta_type) :: td

! Legacy: days present -> "X days, HH:MM:SS"
td = timedelta(days=30, hours=1, minutes=30)
call check(error, &
format_timedelta(td) == '30 days, 01:30:00', &
"timedelta format should be '30 days, 01:30:00'")
"[30d 1h 30m] -> '30 days, 01:30:00'")
if (allocated(error)) return

! Days + hours + seconds
td = timedelta(days=2, hours=5, seconds=30)
call check(error, &
format_timedelta(td) == '2 days, 05:00:30', &
"[2d 5h 30s] -> '2 days, 05:00:30'")
if (allocated(error)) return

! Days + milliseconds: legacy days format keeps .mmm suffix
td = timedelta(days=2, hours=5, seconds=30, milliseconds=125)
call check(error, &
format_timedelta(td) == '2 days, 05:00:30.125', &
"[2d 5h 30s 125ms] -> '2 days, 05:00:30.125'")
if (allocated(error)) return

! No days: hours only
td = timedelta(hours=1, minutes=2, seconds=3)
call check(error, &
format_timedelta(td) == '01:02:03', &
"[1h 2m 3s] -> '01:02:03'")
if (allocated(error)) return

! No days, no hours: MM:SS
td = timedelta(minutes=5, seconds=30)
call check(error, &
format_timedelta(td) == '05:30', &
"[5m 30s] -> '05:30'")
if (allocated(error)) return

! With milliseconds
td = timedelta(hours=1, minutes=2, seconds=3, milliseconds=456)
call check(error, &
format_timedelta(td) == '01:02:03.456', &
"[1h 2m 3s 456ms] -> '01:02:03.456'")
if (allocated(error)) return
end subroutine test_format_timedelta

subroutine test_format_timedelta_compact(error)
type(error_type), allocatable, intent(out) :: error
type(timedelta_type) :: td

! Sub-second: milliseconds only
td = timedelta(milliseconds=5)
call check(error, &
format_timedelta(td) == '5ms', &
"[5ms] -> '5ms'")
if (allocated(error)) return

! Sub-second: zero
td = timedelta(milliseconds=0)
call check(error, &
format_timedelta(td) == '0s', &
"[0ms] -> '0s'")
if (allocated(error)) return

! Sub-minute: seconds with ms fraction
td = timedelta(milliseconds=1500)
call check(error, &
format_timedelta(td) == '1.500s', &
"[1500ms] -> '1.500s'")
if (allocated(error)) return

! Sub-minute: whole seconds
td = timedelta(seconds=42)
call check(error, &
format_timedelta(td) == '42s', &
"[42s] -> '42s'")
if (allocated(error)) return

! Minute-second compact form with milliseconds
td = timedelta(minutes=5, seconds=30, milliseconds=125)
call check(error, &
format_timedelta(td) == '05:30.125', &
"[5m 30s 125ms] -> '05:30.125'")
if (allocated(error)) return

! Negative duration: preserves original format since days /= 0
td = timedelta(seconds=-1)
call check(error, &
format_timedelta(td) == '-1 days, 23:59:59', &
"[-1s] -> '-1 days, 23:59:59' (negative via days component)")
if (allocated(error)) return
end subroutine test_format_timedelta_compact

subroutine test_timedelta_ms_rollover(error)
type(error_type), allocatable, intent(out) :: error
type(datetime_type) :: dt, res
Expand Down
Loading