Huh? 😕



Well, a mechanic usually wouldn’t give you a time estimate in seconds, but a tool I am using prints something like this at the end:

The simulation took 54227.9 seconds in CPU time.

That triggered me to write a “little” script to convert seconds to human time i.e. time in days, hours, minutes and seconds.

Updated code.

Thanks to /u/xiongtx from Reddit, I learned about the built-in function format-seconds that does what I wanted to do – but not exactly in a way I wanted to see. Though, format-seconds gave me an idea for a big optimization (code commit diff).

Below Code section is updated to reflect that. If you like, you can review older version of the same section at the end of this post. Also, at the end, you will find a comparison between the outputs from format-seconds and modi/seconds-to-human-time .

Here’s the updated code, and notes about that follow after that:

( defun modi/seconds-to-human-time ( &optional seconds ) "Convert SECONDS to \"DDd HHh MMm SSs\" string. SECONDS is a non-negative integer or fractional number. SECONDS can also be a list of such numbers, which is the case when this function is called recursively. When called interactively, if a region is selected SECONDS is extracted from that, else the user is prompted to enter those." ( interactive ) ( let (( inter ( called-interactively-p 'interactive ))) ( when inter ( let (( seconds-str ( if ( use-region-p ) ( buffer-substring-no-properties ( region-beginning ) ( region-end )) ( read-string "Enter seconds: " )))) ( setq seconds ( string-to-number seconds-str )))) ;"1" -> 1, "1.2" -> 1.2, "" -> 0 ( let* (( MINUTE 60 ) ( HOUR ( * 60 MINUTE )) ( DAY ( * 24 HOUR )) ( sec ( cond (( listp seconds ) ;This is entered only by recursive calls ( car ( last seconds ))) (( and ( numberp seconds ) ;This is entered only in the first entry ( >= seconds 0 )) seconds ) ( t ( user-error "Invalid argument %S" seconds )))) ( gen-time-string ( lambda ( time inter ) "Return string representation of TIME. TIME is of the type (DD HH MM SS), where each of those elements are numbers. If INTER is non-nil, echo the time string in a well-formatted manner instead of returning it." ( let (( filler " " ) ( str "" )) ( dolist ( unit ' ( "d" "h" "m" "s" )) ( let* (( val ( car ( rassoc unit time ))) ( val-str ( cond (( and ( string= unit "s" ) ;0 seconds ( = val 0 ) ( string-match-p "\\ `\\s-*\\' " str )) " 0s" ) (( and ( string= unit "s" ) ( > val 0 )) ( if ( integerp val ) ( format "%2d%s" val unit ) ( format "%5.2f%s" val unit ))) (( and val ( > val 0 )) ( format "%2d%s " val unit )) ( t filler )))) ( setq str ( concat str val-str )))) ;; (message "debug: %S" time) ( if inter ( message "%0.2f seconds → %s" seconds ( string-trim ( replace-regexp-in-string " +" " " str ))) ( string-trim-right str ))))) ( time ( cond (( >= sec DAY ) ;> day ( let* (( days ( / ( floor sec ) DAY )) ( rem ( - sec ( * days DAY )))) ;; Note that (list rem) instead of just `rem' is ;; being passed to the recursive call to ;; `modi/seconds-to-human-time'. This helps us ;; distinguish between direct and re-entrant ;; calls to this function. ( append ( list ( cons days "d" )) ( modi/seconds-to-human-time ( list rem ))))) (( >= sec HOUR ) ;> hour AND < day ( let* (( hours ( / ( floor sec ) HOUR )) ( rem ( - sec ( * hours HOUR )))) ( append ( list ( cons hours "h" )) ( modi/seconds-to-human-time ( list rem ))))) (( >= sec MINUTE ) ;> minute AND < hour ( let* (( mins ( / ( floor sec ) MINUTE )) ( rem ( - sec ( * mins MINUTE )))) ( append ( list ( cons mins "m" )) ( modi/seconds-to-human-time ( list rem ))))) ( t ;< minute ( list ( cons sec "s" )))))) ;; If `seconds' is a number and not a list, this is *not* a ;; recursive call. Return the time as a string only then. For ;; re-entrant executions, return the `time' list instead. ( if ( numberp seconds ) ( funcall gen-time-string time inter ) time ))))

Code Snippet 1 : Seconds to Human Time

Most of this snippet is just the day/hour/minute/second math. Apart from that, here are some points that I found of interest:

I did not always want to prompt the user to enter the input argument. If a region was selected, the function assumes that the user selected a number, and skips the prompt step. So I used a plain (interactive) form instead of using (interactive "sPrompt: ") or (interactive "r") . See (eintr) Interactive Options and (elisp) Interactive Codes to learn about interactive and its codes.

form instead of using or . See (eintr) Interactive Options and (elisp) Interactive Codes to learn about and its codes. Instead of in-lining a modular chunk of logic, like the one where I convert a list like (1 2 3 4) into "1d 2h 3m 4s" , I assigned it to a let-bound symbol gen-time-string . That allowed the logic to be more discernible when used in: ( if ( numberp seconds ) ( funcall gen-time-string time inter ) time ) Also interesting is the fact that these let-bound lambdas can have their own doc-strings too.

I make use of recursion in this function! But I needed this function to return a string (using that gen-time-string function) only when all the nested calls to itself were returned. So to distinguish between a direct call to the function, and re-entrant calls, when doing the latter, I make the input number a list of that number. So while the function might take an input number like 7 for a direct call, that same number, when needed to call to a recursive call, would get passed as (list 7) or '(7) . If you glance back as that little snippet above, I return the time as a string only if the input seconds is a number — and not a list i.e. only when I am in the “direct call instance”.

The internal variable time is now an alist and can have up to 4 cons elements. Each cons is of the type (TIMEVALUE . TIMEUNIT) . So time now looks like ((DAYS . "d") (HOURS . "h") (MINUTES . "m") (SECONDS . "s")) . If the input seconds is 7200 seconds i.e. 2 hours, I cannot allow time to be just (2) , because then I wouldn’t know the unit of that 2 (2 days? 2 hours? ..). With the above technique to tag the time value with its unit (inspired from format-seconds ), the time value will be set as ((2 . "h")) instead. That way, it would read clearly as 2 hours, 0 minutes, and 0 seconds.

Back inside gen-time-string , I then skip printing the time units that are 0 (unless everything is 0, in which case I print "0s" ). (( and val ( > val 0 )) ( format "%2d%s " val unit )) ( t filler ) ;`filler' is just white-space So instead of printing "1d 0h 0m 5s" , it would print "1d 5s" .

The test generator did not need to be updated, because the code optimization was completely internal — Return values were not affected.

A code isn’t complete without tests!

As much fun I had writing the above function, I had equal fun in writing its little tester too.

( let* (( rand-bool ( lambda () "(random 2) will return either 1 or 0, so frac will be either t or nil" ( = 1 ( random 2 )))) ( count 0 ) ( secs ' ( 0 1 60 61 3600 3601 3660 3661 86400 86401 86460 86461 90000 90001 90060 90061 )) ( len-secs ( length secs )) ( secs-rand1 ( mapcar ( lambda ( s ) ( let (( add-sec ( funcall rand-bool )) ( add-min ( funcall rand-bool )) ( add-hr ( funcall rand-bool )) ( add-day ( funcall rand-bool ))) ( when add-sec ( setq s ( + s 1 ))) ( when add-min ( setq s ( + s 60 ))) ( when add-hr ( setq s ( + s ( * 60 60 )))) ( when add-day ( setq s ( + s ( * 60 60 24 )))) s )) secs )) secs-rand2 ) ( dotimes ( _ ( * 2 len-secs )) ( let* (( frac ( funcall rand-bool )) ( sec ( if frac ( / ( random 100000000 ) 100.00 ) ( random 1000000 )))) ( push sec secs-rand2 ))) ( dolist ( sec ( append secs secs-rand1 secs-rand2 )) ( message "%9.2f seconds → %s" sec ( modi/seconds-to-human-time sec )) ( cl-incf count ) ( when ( = 0 ( mod count len-secs )) ( message ( make-string 40 ?─ )))))

Code Snippet 2 : Test Generator

The test also makes use of a let-bound lambda, for the rand-bool function which I use to randomly return t or nil .

function which I use to randomly return or . The secs list is a set of directed tests, in which the day, hour, minute and second units in time get set to 1 in all possible combinations. (If you are into binary numbers, think of 0000 , 0001 , .. up to 1111 .)

list is a set of directed tests, in which the day, hour, minute and second units in get set to in all possible combinations. (If you are into binary numbers, think of , , .. up to .) The secs-rand1 is a partly randomized version of secs where one or more of the above time units would get randomly added by 1.

is a partly randomized version of where one or more of the above time units would get randomly added by 1. The secs-rand2 is a totally randomized list of time in seconds where the time could be anywhere in the [0, 1000000) range, fractional times with 2 decimal places included.

Test Output #

Upon evaluating both Code Snippet 1 and Code Snippet 2, you will get an output like below:

0.00 seconds → 0s 1.00 seconds → 1s 60.00 seconds → 1m 61.00 seconds → 1m 1s 3600.00 seconds → 1h 3601.00 seconds → 1h 1s 3660.00 seconds → 1h 1m 3661.00 seconds → 1h 1m 1s 86400.00 seconds → 1d 86401.00 seconds → 1d 1s 86460.00 seconds → 1d 1m 86461.00 seconds → 1d 1m 1s 90000.00 seconds → 1d 1h 90001.00 seconds → 1d 1h 1s 90060.00 seconds → 1d 1h 1m 90061.00 seconds → 1d 1h 1m 1s ──────────────────────────────────────── 60.00 seconds → 1m 86402.00 seconds → 1d 2s 86521.00 seconds → 1d 2m 1s 3722.00 seconds → 1h 2m 2s 3661.00 seconds → 1h 1m 1s 90061.00 seconds → 1d 1h 1m 1s 90121.00 seconds → 1d 1h 2m 1s 93662.00 seconds → 1d 2h 1m 2s 172861.00 seconds → 2d 1m 1s 176462.00 seconds → 2d 1h 1m 2s 176520.00 seconds → 2d 1h 2m 86521.00 seconds → 1d 2m 1s 176460.00 seconds → 2d 1h 1m 90062.00 seconds → 1d 1h 1m 2s 93660.00 seconds → 1d 2h 1m 93661.00 seconds → 1d 2h 1m 1s ──────────────────────────────────────── 429733.00 seconds → 4d 23h 22m 13s 902957.30 seconds → 10d 10h 49m 17.30s 684313.07 seconds → 7d 22h 5m 13.07s 62058.42 seconds → 17h 14m 18.42s 799077.55 seconds → 9d 5h 57m 57.55s 347952.39 seconds → 4d 39m 12.39s 31041.30 seconds → 8h 37m 21.30s 242839.97 seconds → 2d 19h 27m 19.97s 852518.67 seconds → 9d 20h 48m 38.67s 160038.24 seconds → 1d 20h 27m 18.24s 689297.00 seconds → 7d 23h 28m 17s 64048.00 seconds → 17h 47m 28s 870956.98 seconds → 10d 1h 55m 56.98s 608767.00 seconds → 7d 1h 6m 7s 167796.00 seconds → 1d 22h 36m 36s 114940.07 seconds → 1d 7h 55m 40.07s ──────────────────────────────────────── 106163.46 seconds → 1d 5h 29m 23.46s 701980.00 seconds → 8d 2h 59m 40s 258706.73 seconds → 2d 23h 51m 46.73s 33609.98 seconds → 9h 20m 9.98s 639774.63 seconds → 7d 9h 42m 54.63s 338533.00 seconds → 3d 22h 2m 13s 365910.00 seconds → 4d 5h 38m 30s 140002.00 seconds → 1d 14h 53m 22s 365024.20 seconds → 4d 5h 23m 44.20s 497072.00 seconds → 5d 18h 4m 32s 304307.67 seconds → 3d 12h 31m 47.67s 337126.00 seconds → 3d 21h 38m 46s 711862.00 seconds → 8d 5h 44m 22s 746474.22 seconds → 8d 15h 21m 14.22s 200503.00 seconds → 2d 7h 41m 43s 952391.00 seconds → 11d 33m 11s ────────────────────────────────────────

You can find the latest version of this code at seconds-to-human-time.el (first revision).

Your car will be ready in 2h 13m 20s, and the simulation took 15h 3m 47.90s in CPU time. Figure 1: Screenshot of seconds-to-human-time.el in Emacs

§

Test output using format-seconds #

Instead of using (modi/seconds-to-human-time sec) in the test generator, if I use the below form using format-seconds to get as close as to what I want:

( message "%9.2f seconds → %s" sec ( replace-regexp-in-string " days?" "d" ( replace-regexp-in-string " hours?" "h" ( replace-regexp-in-string " minutes?" "m" ( replace-regexp-in-string " seconds?" "s" ( format-seconds "%2D %2H %2M %z%2S" sec ))))))

I get the output on the left below. For brevity, I have pasted only few snippets of the whole test for comparison:

0.00 seconds → 0s 0.00 seconds → 0s 1.00 seconds → 1s 1.00 seconds → 1s 60.00 seconds → 1m 0s 60.00 seconds → 1m 61.00 seconds → 1m 1s 61.00 seconds → 1m 1s 3600.00 seconds → 1h 0m 0s 3600.00 seconds → 1h 3661.00 seconds → 1h 1m 1s 3661.00 seconds → 1h 1m 1s 86400.00 seconds → 1d 0h 0m 0s 86400.00 seconds → 1d 86401.00 seconds → 1d 0h 0m 1s 86401.00 seconds → 1d 1s 86460.00 seconds → 1d 0h 1m 0s 86460.00 seconds → 1d 1m 86461.00 seconds → 1d 0h 1m 1s 86461.00 seconds → 1d 1m 1s 90000.00 seconds → 1d 1h 0m 0s 90000.00 seconds → 1d 1h 90001.00 seconds → 1d 1h 0m 1s 90001.00 seconds → 1d 1h 1s 90060.00 seconds → 1d 1h 1m 0s 90060.00 seconds → 1d 1h 1m 90061.00 seconds → 1d 1h 1m 1s 90061.00 seconds → 1d 1h 1m 1s # Below the random numbers are different on both sides, but the thing to note is the loss # fractional values (on the left) when seconds are not integers. 288128.50 seconds → 3d 8h 2m 8s 902957.30 seconds → 10d 10h 49m 17.30s 989679.28 seconds → 11d 10h 54m 39s 684313.07 seconds → 7d 22h 5m 13.07s 803137.00 seconds → 9d 7h 5m 37s 347952.39 seconds → 4d 39m 12.39s 39361.00 seconds → 10h 56m 1s 689297.00 seconds → 7d 23h 28m 17s

Code Snippet 3 format-seconds (Left), using modi/seconds-to-human-time (Right) : Using), using

Notice the redundant 0h , 0m , 0s on the left, and also the loss of seconds precision (the latter point is not a big deal though).

Here’s the code, and notes about that follow after that:

( defun modi/seconds-to-human-time ( &optional seconds ) "Convert SECONDS to \"DDd HHh MMm SSs\" string. SECONDS is a non-negative integer or fractional number. SECONDS can also be a list of such numbers, which is the case when this function is called recursively. When called interactively, if a region is selected SECONDS is extracted from that, else the user is prompted to enter those." ( interactive ) ( let (( inter ( called-interactively-p 'interactive ))) ( when inter ( let (( seconds-str ( if ( use-region-p ) ( buffer-substring-no-properties ( region-beginning ) ( region-end )) ( read-string "Enter seconds: " )))) ( setq seconds ( string-to-number seconds-str )))) ;"1" -> 1, "1.2" -> 1.2, "" -> 0 ( let* (( MINUTE 60 ) ( HOUR ( * 60 MINUTE )) ( DAY ( * 24 HOUR )) ( sec ( cond (( listp seconds ) ;This is entered only by recursive calls ( car ( last seconds ))) (( and ( numberp seconds ) ;This is entered only in the first entry ( >= seconds 0 )) seconds ) ( t ( user-error "Invalid argument %S" seconds )))) ( gen-time-string ( lambda ( time inter ) "Return string representation of TIME. TIME is of the type (DD HH MM SS), where each of those elements are numbers. If INTER is non-nil, echo the time string in a well-formatted manner instead of returning it." ( let* (( rev-time ( reverse time )) ( sec ( nth 0 rev-time )) ( min ( nth 1 rev-time )) ( hr ( nth 2 rev-time )) ( day ( nth 3 rev-time )) ( filler " " ) ( sec-str ( cond (( > sec 0 ) ( if ( integerp sec ) ( format "%2ds" sec ) ( format "%5.2fs" sec ))) (( and ( = sec 0 ) ( null min ) ( null hr ) ( null day )) ;0 seconds " 0s" ))) ( min-str ( if ( and min ( > min 0 )) ( format "%2dm " min ) filler )) ( hr-str ( if ( and hr ( > hr 0 )) ( format "%2dh " hr ) filler )) ( day-str ( if ( and day ( > day 0 )) ( format "%2dd " day ) filler )) ( str ( string-trim-right ( concat day-str hr-str min-str sec-str )))) ( if inter ( message "%0.2f seconds → %s" seconds ( string-trim ( replace-regexp-in-string " +" " " str ))) str )))) ( time ( cond (( >= sec DAY ) ;> day ( let* (( days ( / ( floor sec ) DAY )) ( rem ( - sec ( * days DAY )))) ( cond (( = rem 0 ) ( list days 0 0 0 )) (( < rem MINUTE ) ;; Note that (list rem) instead of just `rem' is being ;; passed to the recursive call to ;; `modi/seconds-to-human-time'. This helps us ;; distinguish between direct and re-entrant calls to ;; this function. ( append ( list days 0 0 ) ( modi/seconds-to-human-time ( list rem )))) (( < rem HOUR ) ( append ( list days 0 ) ( modi/seconds-to-human-time ( list rem )))) ( t ( append ( list days ) ( modi/seconds-to-human-time ( list rem ))))))) (( >= sec HOUR ) ;> hour AND < day ( let* (( hours ( / ( floor sec ) HOUR )) ( rem ( - sec ( * hours HOUR )))) ( cond (( = rem 0 ) ( list hours 0 0 )) (( < rem MINUTE ) ( append ( list hours 0 ) ( modi/seconds-to-human-time ( list rem )))) ( t ( append ( list hours ) ( modi/seconds-to-human-time ( list rem ))))))) (( >= sec MINUTE ) ;> minute AND < hour ( let* (( mins ( / ( floor sec ) MINUTE )) ( rem ( - sec ( * mins MINUTE )))) ( cond (( = rem 0 ) ( list mins 0 )) ( t ( append ( list mins ) ( modi/seconds-to-human-time ( list rem ))))))) ( t ;< minute ( list sec ))))) ;; If `seconds' is a number and not a list, this is *not* a recursive ;; call. Return the time as a string only then. For re-entrant ;; executions, return the `time' list instead. ( if ( numberp seconds ) ( funcall gen-time-string time inter ) time ))))

Code Snippet 4 : Seconds to Human Time (Revision 1)

Most of this snippet is just the day/hour/minute/second math. Apart from that, here are some points that I found of interest: