Skip to content

Commit a700b84

Browse files
committed
datetime: add compact timedelta formatting for zero-day durations
1 parent 4c8521d commit a700b84

4 files changed

Lines changed: 143 additions & 13 deletions

File tree

doc/specs/stdlib_datetime.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,14 +243,22 @@ Pure function.
243243
#### Description
244244

245245
Formats a `timedelta_type` as a human-readable string.
246+
For zero-day durations, compact forms are used:
247+
- `5ms` for sub-second values
248+
- `1.500s` or `42s` for sub-minute values
249+
- `MM:SS` when hours are zero
250+
- `HH:MM:SS[.mmm]` otherwise
251+
252+
For nonzero-day durations, the verbose form is preserved:
253+
`X days, HH:MM:SS[.mmm]`.
246254

247255
#### Syntax
248256

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

251259
#### Return value
252260

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

255263
## Utility Functions
256264

example/datetime/example_datetime_usage.f90

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,19 @@ program example_datetime
6565
t3 = datetime_type(2026, 3, 17, 17, 30, 0, 0, 330)
6666
print '(A,L1)', '12:00Z == 17:30+05:30? ', t2 == t3
6767

68-
! 9. Unix epoch
68+
! 9. Compact format_timedelta (sub-second / sub-minute)
69+
print *
70+
print '(A)', '=== Compact Duration Formatting ==='
71+
duration = timedelta(milliseconds=5)
72+
print '(A,T40,A)', 'timedelta(ms=5):', format_timedelta(duration)
73+
duration = timedelta(milliseconds=1500)
74+
print '(A,T40,A)', 'timedelta(ms=1500):', format_timedelta(duration)
75+
duration = timedelta(seconds=65)
76+
print '(A,T40,A)', 'timedelta(s=65):', format_timedelta(duration)
77+
duration = timedelta(hours=1, minutes=2, seconds=3)
78+
print '(A,T40,A)', 'timedelta(1h 2m 3s):', format_timedelta(duration)
79+
80+
! 10. Unix epoch
6981
print *
7082
print '(A,A)', 'Unix epoch: ', &
7183
format_datetime(epoch())

src/datetime/stdlib_datetime.f90

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -340,21 +340,59 @@ pure function format_timedelta(td) result(str)
340340
!! version: experimental
341341
!!
342342
!! Format a timedelta_type as a readable string.
343+
!! When days == 0, sub-second/sub-minute durations are formatted
344+
!! compactly (e.g. "5ms", "1.500s", "05:30"). The "0 days, "
345+
!! prefix is omitted. When days /= 0, original verbose format
346+
!! is preserved.
343347
type(timedelta_type), intent(in) :: td
344348
character(:), allocatable :: str
345-
integer :: h, m, s
349+
integer :: h, m, s, ms
350+
351+
h = td%seconds / 3600
352+
m = mod(td%seconds, 3600) / 60
353+
s = mod(td%seconds, 60)
354+
ms = td%milliseconds
355+
356+
if (td%days == 0) then
357+
! Compact formatting for zero-day durations
358+
if (h == 0 .and. m == 0 .and. s == 0) then
359+
if (ms == 0) then
360+
str = "0s"
361+
else
362+
str = to_string(ms, '(I0)') // "ms"
363+
end if
364+
return
365+
end if
346366

347-
h = td%seconds / 3600
348-
m = mod(td%seconds, 3600) / 60
349-
s = mod(td%seconds, 60)
367+
if (h == 0 .and. m == 0) then
368+
if (ms == 0) then
369+
str = to_string(s, '(I0)') // "s"
370+
else
371+
str = to_string(s, '(I0)') // "." // to_string(ms, '(I3.3)') // "s"
372+
end if
373+
return
374+
end if
350375

351-
str = to_string(td%days, '(I0)') // ' days, ' // &
352-
to_string(h, '(I2.2)') // ':' // &
353-
to_string(m, '(I2.2)') // ':' // &
354-
to_string(s, '(I2.2)')
376+
if (h > 0) then
377+
str = to_string(h, '(I2.2)') // ":" // &
378+
to_string(m, '(I2.2)') // ":" // &
379+
to_string(s, '(I2.2)')
380+
else
381+
str = to_string(m, '(I2.2)') // ":" // &
382+
to_string(s, '(I2.2)')
383+
end if
355384

356-
if (td%milliseconds /= 0) then
357-
str = str // '.' // to_string(td%milliseconds, '(I3.3)')
385+
if (ms /= 0) str = str // "." // to_string(ms, '(I3.3)')
386+
else
387+
! Original format for durations with days
388+
str = to_string(td%days, '(I0)') // ' days, ' // &
389+
to_string(h, '(I2.2)') // ':' // &
390+
to_string(m, '(I2.2)') // ':' // &
391+
to_string(s, '(I2.2)')
392+
393+
if (ms /= 0) then
394+
str = str // '.' // to_string(ms, '(I3.3)')
395+
end if
358396
end if
359397
end function format_timedelta
360398

test/datetime/test_datetime.f90

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ subroutine collect_datetime(testsuite)
7171
test_format_datetime_offset), &
7272
new_unittest("format_timedelta_test", &
7373
test_format_timedelta), &
74+
new_unittest("format_timedelta_compact", &
75+
test_format_timedelta_compact), &
7476
new_unittest("timedelta_ms_rollover", &
7577
test_timedelta_ms_rollover), &
7678
new_unittest("to_utc_test", &
@@ -505,13 +507,83 @@ end subroutine test_format_datetime_offset
505507
subroutine test_format_timedelta(error)
506508
type(error_type), allocatable, intent(out) :: error
507509
type(timedelta_type) :: td
510+
511+
! Legacy: days present -> "X days, HH:MM:SS"
508512
td = timedelta(days=30, hours=1, minutes=30)
509513
call check(error, &
510514
format_timedelta(td) == '30 days, 01:30:00', &
511-
"timedelta format should be '30 days, 01:30:00'")
515+
"[30d 1h 30m] -> '30 days, 01:30:00'")
516+
if (allocated(error)) return
517+
518+
! Days + hours + seconds
519+
td = timedelta(days=2, hours=5, seconds=30)
520+
call check(error, &
521+
format_timedelta(td) == '2 days, 05:00:30', &
522+
"[2d 5h 30s] -> '2 days, 05:00:30'")
523+
if (allocated(error)) return
524+
525+
! No days: hours only
526+
td = timedelta(hours=1, minutes=2, seconds=3)
527+
call check(error, &
528+
format_timedelta(td) == '01:02:03', &
529+
"[1h 2m 3s] -> '01:02:03'")
530+
if (allocated(error)) return
531+
532+
! No days, no hours: MM:SS
533+
td = timedelta(minutes=5, seconds=30)
534+
call check(error, &
535+
format_timedelta(td) == '05:30', &
536+
"[5m 30s] -> '05:30'")
537+
if (allocated(error)) return
538+
539+
! With milliseconds
540+
td = timedelta(hours=1, minutes=2, seconds=3, milliseconds=456)
541+
call check(error, &
542+
format_timedelta(td) == '01:02:03.456', &
543+
"[1h 2m 3s 456ms] -> '01:02:03.456'")
512544
if (allocated(error)) return
513545
end subroutine test_format_timedelta
514546

547+
subroutine test_format_timedelta_compact(error)
548+
type(error_type), allocatable, intent(out) :: error
549+
type(timedelta_type) :: td
550+
551+
! Sub-second: milliseconds only
552+
td = timedelta(milliseconds=5)
553+
call check(error, &
554+
format_timedelta(td) == '5ms', &
555+
"[5ms] -> '5ms'")
556+
if (allocated(error)) return
557+
558+
! Sub-second: zero
559+
td = timedelta(milliseconds=0)
560+
call check(error, &
561+
format_timedelta(td) == '0s', &
562+
"[0ms] -> '0s'")
563+
if (allocated(error)) return
564+
565+
! Sub-minute: seconds with ms fraction
566+
td = timedelta(milliseconds=1500)
567+
call check(error, &
568+
format_timedelta(td) == '1.500s', &
569+
"[1500ms] -> '1.500s'")
570+
if (allocated(error)) return
571+
572+
! Sub-minute: whole seconds
573+
td = timedelta(seconds=42)
574+
call check(error, &
575+
format_timedelta(td) == '42s', &
576+
"[42s] -> '42s'")
577+
if (allocated(error)) return
578+
579+
! Negative duration: preserves original format since days /= 0
580+
td = timedelta(seconds=-1)
581+
call check(error, &
582+
format_timedelta(td) == '-1 days, 23:59:59', &
583+
"[-1s] -> '-1 days, 23:59:59' (negative via days component)")
584+
if (allocated(error)) return
585+
end subroutine test_format_timedelta_compact
586+
515587
subroutine test_timedelta_ms_rollover(error)
516588
type(error_type), allocatable, intent(out) :: error
517589
type(datetime_type) :: dt, res

0 commit comments

Comments
 (0)