Your car will be ready in 8000 seconds
— Kaushal Modi- Updated code.
ย ย ย ย 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.
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
.
Code #
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))))
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 aboutinteractive
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 symbolgen-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 inputseconds
is a number — and not a list i.e. only when I am in the “direct call instance”.
- So while the function might take an input number like
The internal variable
time
is now an alist and can have up to 4 cons elements. Each cons is of the type(TIMEVALUE . TIMEUNIT)
. Sotime
now looks like((DAYS . "d") (HOURS . "h") (MINUTES . "m") (SECONDS . "s"))
.If the input
seconds
is 7200 seconds i.e. 2 hours, I cannot allowtime
to be just(2)
, because then I wouldn’t know the unit of that2
(2 days? 2 hours? ..). With the above technique to tag the time value with its unit (inspired fromformat-seconds
), thetime
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"
.
Tests #
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 ?โ)))))
- The test also makes use of a let-bound lambda, for the
rand-bool
function which I use to randomly returnt
ornil
. - The
secs
list is a set of directed tests, in which the day, hour, minute and second units intime
get set to1
in all possible combinations. (If you are into binary numbers, think of0000
,0001
, .. up to1111
.) - The
secs-rand1
is a partly randomized version ofsecs
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
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Source #
You can find the latest version of this code at
seconds-to-human-time.el
(first revision).
Closing #
Your car will be ready in 2h 13m 20s, and the simulation took 15h 3m 47.90s in CPU time.
Appendix #
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
- 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).
Code (Revision 1) #
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))))
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 aboutinteractive
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 symbolgen-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 inputseconds
is a number — and not a list i.e. only when I am in the “direct call instance”.
- So while the function might take an input number like
The internal variable
time
is a list and can have up to 4 number elements:(DAYS HOURS MINUTES SECONDS)
. The key was to always have each of those elements at their respective positions in the list.If the input
seconds
is 7200 seconds i.e. 2 hours, I cannot allowtime
to be just(2)
, because then I wouldn’t know the unit of that2
(2 days? 2 hours? ..) — The nestedcond
logic for setting thetime
variable ensures it gets set to(2 0 0)
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"
). So instead of printing"1d 0h 0m 5s"
, it would print just"1d 5s"
.