4

I want to subtract time values like 12:04 from 11:30 and get 34 minutes or 0:34.

I tried to achieve this using calculations like

(time-subtract 
 (parse-time-string "12:04") 
 (parse-time-string "11:30"))

This way I get the error Invalid time specification.

OK. Maybe I need to encode times first:

(time-subtract 
 (apply #'encode-time (parse-time-string "12:04"))
 (apply #'encode-time (parse-time-string "11:30")))

Now I get the error Wrong type argument: fixnump, nil.

Is there a way to subtract times without having to calculate total minutes of both times first like 12 * 60 + 4?

1 Answer 1

7

This way I get the error Invalid time specification.

OK. Maybe I need to encode times first:

Indeed.

Now I get the error Wrong type argument: fixnump, nil.

This is because parse-time-string is documented as returning nil for unknown entries, such as those for the date (emphasis mine):

parse-time-string is a compiled Lisp function in `parse-time.el'.

(parse-time-string STRING)

  Probably introduced at or before Emacs version 27.1.

Parse the time in STRING into (SEC MIN HOUR DAY MON YEAR DOW DST TZ).
STRING should be an ISO 8601 time string, e.g., "2020-01-15T16:12:21-08:00",
or something resembling an RFC 822 (or later) date-time, e.g.,
"Wed, 15 Jan 2020 16:12:21 -0800".  This function is
somewhat liberal in what format it accepts, and will attempt to
return a "likely" value even for somewhat malformed strings.
The values returned are identical to those of `decode-time', but
any unknown values other than DST are returned as nil, and an
                                      ^^^^^^^^^^^^^^^
unknown DST value is returned as -1.

For example:

(parse-time-string "12:04")
;; => (0 4 12 nil nil nil nil -1 nil)

This result cannot be handled directly by other time functions, so those nils need filling in. Emacs 27 introduced the handy utility function decoded-time-set-defaults for this purpose:

decoded-time-set-defaults is a compiled Lisp function in `time-date.el'.

(decoded-time-set-defaults TIME &optional DEFAULT-ZONE)

Set any nil values in `decoded-time' TIME to default values.
The default value is based on January 1st, 1970 at midnight.

TIME is modified and returned.

Behold:

(decoded-time-set-defaults (parse-time-string "12:04"))
;; => (0 4 12 1 1 0 nil -1 nil)

Putting it all together (note that the new calling convention of encode-time accepts a decoded time as a single list argument):

(require 'time-date)

(defun my-mins-between (time1 time2)
  "Return the minutes between HH:mm strings TIME1 and TIME2."
  (let ((diff (apply #'time-subtract
                     (mapcar (lambda (time)
                               (encode-time (decoded-time-set-defaults
                                             (parse-time-string time))))
                             (list time1 time2)))))
    (/ (float-time diff) 60)))

(my-mins-between "12:04" "11:30")
;; => 34.0

Or you can declare technical bankruptcy and rely on Org parsing functions instead. Since Org 9.1:

(- (org-duration-to-minutes "12:04")
   (org-duration-to-minutes "11:30"))
;; => 34.0

In older Org versions:

(- (org-hh:mm-string-to-minutes "12:04")
   (org-hh:mm-string-to-minutes "11:30"))
;; => 34

(- (org-duration-string-to-minutes "12:04")
   (org-duration-string-to-minutes "11:30"))
;; => 34

References

15
  • 1
    Thanks @Basil! parsing time is one of the corners of elisp that always trips me up. This is a very helpful explanation
    – Tyler
    Commented May 3, 2021 at 22:05
  • Alas! In GNU Emacs 27.1 (build 1, x86_64-w64-mingw32): Debugger entered--Lisp error: (error "Invalid time specification") encode-time((0 30 11 1 1 0 nil -1 nil)) (lambda (time) (encode-time (decoded-time-set-defaults (parse-time-string time))))("11:30") mapcar((lambda (time) (encode-time (decoded-time-set-defaults (parse-time-string time)))) ("11:30" "12:04")) Commented May 4, 2021 at 7:09
  • I declare technical bankruptcy! :-) The best answer for me is (- (org-duration-to-minutes "12:04") (org-duration-to-minutes "11:30")) ;; => 34.0 Commented May 4, 2021 at 7:28
  • @EvgenyMikhaylov If encode-time doesn't work as intended for you, then please try Emacs 27.2, and either way consider reporting it upstream via M-x report-emacs-bug RET.
    – Basil
    Commented May 4, 2021 at 7:44
  • @Basil Thank you for encouraging me to upgrade! It's the bleeding edge for me: GNU Emacs 27.2 (build 1, x86_64-w64-mingw32). But the problem persists: Debugger entered--Lisp error: (error "Invalid time specification") encode-time((0 30 11 1 1 0 nil -1 nil)) Are you sure it's a bug? Commented May 4, 2021 at 20:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.