Friday, May 14, 2010

Time Spans for Humans

When dealing with time spans or durations, it is nice to be able to render elapsed time in a way that is easy for the human eye to read.

In some cases, this could mean translating the way you might say it or write it (two years, ten days, four hours, thirty minutes, and fifteen seconds) or a more concise form (2y 10d 4h 30m 15s). The concise form has become popular in some web applications, and solutions exist for both in most languages including python, perl, and java. I thought I would build a word in Factor to calculate it.

First, we need some other vocabularies:

USING: kernel make math math.parser sequences ;

The make vocabulary can be used to execute a quotation that builds a sequence of values at runtime. This can be a little confusing for those new to Factor. Basically, you provide it a quotation that periodically emits values (by calling the , word) that are collected into the sequence type you provide. For example:

( scratchpad ) [ 1 , 2 , 3 , ] { } make .
{ 1 2 3 }

Next, we need an algorithm. One way is to iteratively divmod the number of seconds into each category (e.g., years, weeks, days, hours, minutes, seconds) and count it if the number in the category is non-zero.

: elapsed-time ( seconds -- string )
    dup 0 < [ "negative seconds" throw ] when [
        {
            { 60 "s" }
            { 60 "m" }
            { 24 "h" }
            {  7 "d" }
            { 52 "w" }
            {  f "y" }
        } [
            [ first [ /mod ] [ dup ] if* ] [ second ] bi swap
            dup 0 > [ number>string prepend , ] [ 2drop ] if
        ] each drop
    ] { } make [ "0s" ] [ reverse " " join ] if-empty ;

And then to see it work:

( scratchpad ) 123456 elapsed-time .
"1d 10h 17m 36s"

3 comments:

Jim Mack (1963) said...

Thank you for these. I learn something every time. I'd like to contribute an alternative. My goal is to learn to read and write in the nested style above, but for now I turn to
factoring the problem into smaller parts, in the hope that it becomes dsl-like.

USING: assocs combinators formatting kernel make math memoize
nested-comments prettyprint qw sequences ;

MEMO: set-sizes ( -- seq )
{ 60 60 24 7 52 1 }
qw{ s m h d w y } zip ;

: sets-in ( total setsize remainder-unit -- whole-sets remainder-description )
[ /mod ] dip "%d%s" sprintf ;

: while-remaining ( seconds set-pair -- seconds )
over zero?
[ drop ] [ first2 sets-in , ] if ;

: collect-set-descriptions ( size-seq seconds -- str )
[ [ while-remaining ] each drop ] { } make
reverse " " join ;

: elapsed-time2 ( seconds -- string )
{
{ [ dup 0 < ] [ drop "negative seconds" throw ] }
{ [ dup zero? ] [ drop "0s" ] }
[ set-sizes collect-set-descriptions ]
} cond ;


123456 elapsed-time2 .
! "1d 10h 17m 36s"

Does the increase in definitions and libraries used justify this approach?

mrjbq7 said...

Thanks for the feedback!

There really is quite a lot going on in my version, and one of the things I like most about Factor is how it encourages making several smaller words that compose nicely into larger functionality, like yours.

That reminds me of a post I've been meaning to make about a lesson Slava gave me once about refactoring some Factor code that is often written by beginners to be more idiomatic.

Jim Mack (1963) said...

Looking forward to reading it.