Formatting durations

See Also

Years, months, days, etc. between two dates
A similar page

Description

The following proc converts an integer number of seconds to culture-dependent units (days, hrs, mins, secs) specification. From a news:comp.lang.tcl posting by David Gravereaux:

Here's a duration proc I wrote that should give you a better sense of what I think you're asking 'clock format' for:

% proc duration int_time {
    set timeList [list]
    foreach div {86400 3600 60 1} mod {0 24 60 60} name {day hr min sec} {
        set n [expr {$int_time / $div}]
        if {$mod > 0} {set n [expr {$n % $mod}]}
        if {$n > 1} {
            lappend timeList "$n ${name}s"
        } elseif {$n == 1} {
            lappend timeList "$n $name"
        }
    }
    return [join $timeList]
}
% duration 67
1 min 7 secs

Updated by DKF


For a version of the same that handles floats and avoids OCTAL, see:

 http://inferno.slug.org/wiki/wiki?Duration

Culture-independent hh:mm:ss notation

By RS:

proc hh:mm:ss {secs} {
    set h [expr {$secs/3600}]
    incr secs [expr {$h*-3600}]
    set m [expr {$secs/60}]
    set s [expr {$secs%60}]
    format "%02.2d:%02.2d:%02.2d" $h $m $s
}
hh:mm:ss 67
00:01:07

Oops, forget it. Tcl has it built in (if you force GMT):

clock format 67 -gmt 1 -format %H:%M:%S   ;#RS
00:01:07

Bah! That chokes on Octal and fractional seconds, Richard!!

- RS: I'm not sure whether we're talking about the same - the original question was to format an integer number of seconds into hour/min/sec format. Fractionals are excluded by the "integer", but octals just work as always:

% clock format 012345 -gmt 1 -format %H:%M:%S   
01:29:09

You can redeem yourself by making spamMap work, however...

Or octals don't work as always, depending on your perspective... I suppose we are talking about two different things; I added support for decimal seconds and stripping the leading 0 from octals because my user base required it.

Misc

KBK: All of the above have problems with dealing with units of variable length. Consider the problem of "what's the day one month after 30 January," or even "what's the time at one day after noon on the day before the clocks go to Daylight Saving Time (Sommerzeit)." For that sort of problem, you need to compute the years, months, days, etc. between two dates.


NEM: A little toy proc I knocked up today - gives you the time since a given point in a vague way:

proc howlongago {time} {
    # Returns the difference between $time and now in vague terms
    set diff [expr {[clock seconds] - $time}]
    # What units are we dealing with (don't care about leap years -
    # we're being vague, after all :)
    foreach {div unit} {
        60*60*24*365        year
        60*60*24*30         month
        60*60*24*7          week
        60*60*24            day
        60*60               hour
        60                  minute
        1                   second
     } {
         if {[set num [expr $diff / ($div)]] > 0} {
             break
         }
     }
     if {$num != 1} { append unit s }
     if {$num == 0} { return now }
     if {$num == 1} { return "a $unit ago" }
     if {$num == 2} { return "a couple of $unit ago" }
     if {$num > 2 && $num < 5} { return "a few $unit ago" }
     return "$num $unit ago"
}

Some tests:

tclsh8.4 [~/tclstuff] howlongago [clock seconds]
now
tclsh8.4 [~/tclstuff] howlongago [expr [clock seconds] - 3]
a few seconds ago
tclsh8.4 [~/tclstuff] howlongago [expr [clock seconds] - 20]
20 seconds ago
tclsh8.4 [~/tclstuff] howlongago [clock scan "1 January 2003"]
a month ago
tclsh8.4 [~/tclstuff] howlongago [clock scan "1 January 2002"]
a year ago
tclsh8.4 [~/tclstuff] howlongago [clock scan "1 November 2002"]
a few months ago
tclsh8.4 [~/tclstuff] howlongago [clock scan "1 December 2002"]
a couple of months ago

Second Formatter - similar to clock format, but instead of a time it works on just a number of seconds. The codes for seconds/minutes/hours/days are %s, %m, %h and %d, with the upper-case of each left-padding with 0's to make it two characters long. - MG (moved from Bag of number/time spellers and given a default format, Jun 04) Apr 29th 2004 - improved to remove the un-needed 'pad' proc

 proc timefmt {secs {fmt "%H:%M:%S"}} {
if { ![string is integer -strict $secs] } {
       error "first argument must be integer"
     }

  set d [expr {$secs/86400}] ; set D [string range "0$d" end-1 end]
  set h [expr {($secs%86400)/3600}] ; set H [string range "0$h" end-1 end]
  set m [expr {(($secs%86400)%3600)/60}] ; set M [string range "0$m" end-1 end]
  set s [expr {(($secs%86400)%3600)%60}] ; set S [string range "0$s" end-2 end]

  set p "%% % %s $s %S $S %m $m %M $M %h $h %H $H %d $d %D $D"
  set str [string map $p $fmt]
  return $str;

};# timefmt

% timefmt 527 {%h:%M:%S}
0:08:47
% timefmt 527
00:08:47
% timefmt 999 {There are %d days, %h hours, %m minutes and %s seconds}
There are 0 days, 0 hours, 16 minutes and 39 seconds

FPX: Why invent your own formatting, when there are perfectly good standards out there, such as ISO 8601 . It may not be as humanly readable, but it's well-defined and precise.

Because, as hard as it is to believe by some computer professionals, quite often the main purpose of software is to make information humanly readable in spite of standards and internal formats.


jnc: How about parsing hh:mm:ss and returning the number of seconds? Possibly even dd:hh:mm:ss... Here are a few scenarios I came up with:

proc TimeToSeconds1 t {
    foreach val [lreverse [split $t :]] mul {1 60 3600 86400} {
        if {$val == {}} break
        incr result [expr {[scan $val %d] * $mul}]
    }
    return $result
}
# time {TimeToSeconds1 01:01:01:01} 100000 ;# 17.0662 microseconds
# time {TimeToSeconds1 01:01} 100000 ;# 13.3545 microseconds


proc TimeToSeconds2 t {
    foreach val [scan $t %d:%d:%d:%d] mul {86400 3600 60 1} {
        if {$val == {}} continue
        incr result [expr {$val * $mul}]
    }
    return $result
}
# time {TimeToSeconds2 01:01:01:01} 100000 ;# 10.783 microseconds 
# time {TimeToSeconds2 01:01} 100000 ;# 10.3331 microseconds

NEM See clock scan:

clock scan $time -format %H:%M:%S

MG: Though the clock scan method returns a number of seconds since the epoch for HH:MM:SS past midnight (so to get just a number of seconds represented, you'd need to do something like

expr {[clock scan $time -format %H:%M:%S] - [clock scan 00:00:00 -format %H:%M:%S]}

Also be aware that this method doesn't seem to handle more than 99 hours (presumably b/c %H specifies a two-digit number of hours).

jnc: I guess clock scan will work in most cases. For my particular application, I needed the ability to parse Days, Hours, Minutes, Seconds, or just Minutes, Seconds (10:30), or just seconds (15). None of those cases works with clock scan.

See also: Splitting an amount in parts or Converting human time durations for (partly) doing the reverse.