You can clone with
;; use appt-convert-time instead if appt is loaded
(defun iy/convert-time (time2conv)
"Convert hour:min[am/pm] format TIME2CONV to minutes from midnight.
A period (.) can be used instead of a colon (:) to separate the
hour and minute parts."
;; Formats that should be accepted:
;; 10:00 10.00 10h00 10h 10am 10:00am 10.00am
(let ((min (if (string-match "[h:.]\\([0-9][0-9]\\)" time2conv)
(string-to-number (match-string 1 time2conv))
(hr (if (string-match "[0-9]*[0-9]" time2conv)
(string-to-number (match-string 0 time2conv))
;; Convert the time appointment time into 24 hour time.
(cond ((and (string-match "pm" time2conv) (< hr 12))
(setq hr (+ 12 hr)))
((and (string-match "am" time2conv) (= hr 12))
(setq hr 0)))
;; Convert the actual time into minutes.
(+ (* hr 60) min)))
(defun iy/org-get-entries (files date)
(let (entry entries)
(dolist (file files)
(setq entry (org-agenda-get-day-entries
file date :scheduled :timestamp))
(setq entries (append entry entries))))
(defun sacha/org-calculate-free-time (date start-time end-of-day)
"Return a cons cell of the form (TASK-TIME . FREE-TIME) for DATE, given START-TIME and END-OF-DAY.
DATE is a list of the form (MONTH DAY YEAR).
START-TIME and END-OF-DAY are the number of minutes past midnight."
(let* ((entries (iy/org-get-entries (org-agenda-files) date))
;; For each item on the list
(dolist (entry entries)
(let ((time (get-text-property 1 'time entry))
(effort (org-get-effort (get-text-property 1 'org-hd-marker entry))))
(string-match "\\([^-]+\\)-\\([^-]+\\)" time))
(save-match-data (iy/convert-time (match-string 1 time)))
(save-match-data (iy/convert-time (match-string 2 time))))
(string-match "\\([^-]+\\)\\.+" time)
(not (eq effort nil)))
(let ((start (save-match-data (iy/convert-time (match-string 1 time))))
(effort (save-match-data (iy/convert-time effort))))
(push (cons start (+ start effort)) scheduled-entries)))
((not (eq effort nil))
(setq total-unscheduled (+ (iy/convert-time effort)
;; Sort the scheduled entries by time
(setq scheduled-entries (sort scheduled-entries (lambda (a b) (< (car a) (car b)))))
(let ((start (car (car scheduled-entries)))
(end (cdr (car scheduled-entries))))
;; are we in the middle of this timeslot?
((and (>= last-timestamp start)
(<= last-timestamp end))
;; move timestamp later, no change to time
(setq last-timestamp end))
;; are we completely before this timeslot?
((< last-timestamp start)
;; add gap to total, skip to the end
(setq total-gap (+ (- start last-timestamp) total-gap))
(setq last-timestamp end)))
(setq scheduled-entries (cdr scheduled-entries))))
(if (< last-timestamp end-of-day)
(setq total-gap (+ (- end-of-day last-timestamp) total-gap)))
(cons total-unscheduled total-gap))))