Emacs, scripting and anything text oriented.

Your car will be ready in 8000 seconds

Kaushal Modi

    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.

<2018-02-08 Thu>
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.


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))))
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.
  • 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".

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 ?โ”€)))))
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.
  • 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.)
  • 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.
  • 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 snippets – 1 and 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.

images/seconds-to-human-time.png
ยง

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
Code Snippet 3: Using format-seconds (Left), using modi/seconds-to-human-time (Right)
  • 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))))
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:

  • 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.
  • 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 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 allow time to be just (2), because then I wouldn’t know the unit of that 2 (2 days? 2 hours? ..) — The nested cond logic for setting the time 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".