1 ;;; schedule.el --- calculate schedule completion time
3 ;; Copyright (C) 1999, 2000, 2008 John Wiegley.
5 ;; Author: John Wiegley <johnw@gnu.org>
6 ;; Created: 20 Jan 1999
9 ;; X-URL: http://www.newartisans.com/johnw/Emacs/schedule.el
11 ;; This version of schedule.el is part of Planner. It is not part of
14 ;; Planner is free software; you can redistribute it and/or modify it
15 ;; under the terms of the GNU General Public License as published by
16 ;; the Free Software Foundation; either version 2, or (at your option)
19 ;; Planner is distributed in the hope that it will be useful, but
20 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22 ;; General Public License for more details.
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with Planner; see the file COPYING. If not, write to the
26 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
27 ;; Boston, MA 02110-1301, USA.
31 ;; This file provides a single Lisp function:
32 ;; `schedule-completion-time'. It takes an Emacs time object and a
33 ;; quantity of seconds. It returns an Emacs time object that
34 ;; represents when the given number of seconds will be completed,
35 ;; assuming that work can only be done during work hours.
37 ;; The available work hours are affected by several factors:
39 ;; 1. If timeclock.el is being used, the amount of time left
40 ;; in the current workday (timeclock-workday-remaining)
41 ;; 2. The amount of time in each workday (schedule-workday)
42 ;; 3. The definition of a work week (schedule-week)
43 ;; 4. Any holidays defined in the Emacs calendar
44 ;; 5. Any appointments in the Emacs diary
46 ;; Taking all of the "block out" periods into account,
47 ;; `schedule-completion-time' will compute when the given number of
48 ;; seconds will be done, based on your current definitions of time
51 ;; As an example, here's a function which, given a list of durations
52 ;; in seconds, will return a list of completion times starting from
53 ;; the current moment:
55 ;; (defun compute-completion-times (&rest durations)
56 ;; "Compute completion times for a list of DURATIONS (in seconds)."
57 ;; (let ((now (current-time)))
61 ;; (setq now (schedule-completion-time now dura))))
64 ;; To call this function:
66 ;; (compute-completion-times 3600 7200 3600)
74 (defconst schedule-version
"2.0"
75 "This version of schedule.")
77 (defgroup schedule nil
78 "A mode to help keep track of project schedules."
83 (defvar schedule-week-length nil
84 "The length of the week in seconds.
85 This variable is computed whenever `schedule-week' is set.")
87 (defcustom schedule-workday
(if (boundp 'timeclock-workday
)
90 "*The length of a work period.
91 If the `timeclock' package is being used, it will be consulted in
92 order to determine a proper default value."
96 (defun schedule-duration-to-seconds (code)
97 "Convert the given CODE into a integer quantity of seconds."
98 (if (string-match "\\([0-9.]+\\)\\([smhdw]\\)" code
)
99 (let ((amount (string-to-number (match-string 1 code
)))
100 (kind (match-string 2 code
)))
101 (cond ((equal kind
"s")
108 (* amount schedule-workday
))
110 (* amount schedule-week-length
))
112 (error "Invalid duration code"))))
113 (error "Invalid duration code")))
115 (defvar schedule-computed-week nil
116 "The meaning of `schedule-week', converted into numeric form.")
118 (defun schedule-calculate-week ()
119 "Convert `schedule-week' into `schedule-computed-week'."
121 schedule-week-length
0
122 schedule-computed-week
127 (let ((start (car day
)))
128 (if (functionp start
)
129 (setq start
(funcall start
)))
130 (if (string-match "^\\([0-9]+\\):\\([0-9]+\\)\\([ap]\\)m?$" start
)
131 (let ((hour (string-to-number (match-string 1 start
)))
132 (min (string-to-number (match-string 2 start
)))
133 (ampm (match-string 3 start
)))
134 (if (and (= hour
12) (string= ampm
"a"))
136 (if (and (< hour
12) (string= ampm
"p"))
137 (setq hour
(+ hour
12)))
139 (if (functionp (cadr day
))
141 (schedule-duration-to-seconds (cadr day
)))))
142 (setq schedule-week-length
143 (+ schedule-week-length length
))
144 (list hour min length
)))
146 (symbol-value 'schedule-week
)))) ; to quiet byte compiler
148 (defcustom schedule-week
156 "*A description of what your regular week looks like.
157 The list should be seven elements long, the first entry being for
158 Sunday. Each entry is either nil, or a list of the form (TIME-IN
159 DURATION). TIME-IN should be a string of the form \"11:30a\", or a
160 function returning such a string; DURATION should be a duration
161 string, such as \"8h\", or a function returning a quantity of
163 :set
(lambda (symbol value
)
164 (setq schedule-week value
)
165 (schedule-calculate-week)
167 :type
'(repeat (choice (const :tag
"No work" nil
)
169 (choice (string :tag
"Time in")
171 (choice (string :tag
"Duration")
175 (defcustom schedule-diary-period nil
176 "*How many days at a time we should look through the diary.
177 If you have lots of repeating appointments, things may go faster if
178 you decrease this number. If this variable is set to nil, the diary
180 :set
(lambda (symbol value
)
182 (require 'diary-lib
))
183 (setq schedule-diary-period value
))
184 :type
'(choice (integer :tag
"Quantum of days to examine")
185 (const :tag
"Don't consult diary" nil
))
188 ;;; Internal Variables:
190 (defvar schedule-day-remainder nil
191 "The number of seconds remaining today.
192 Used in calculations only.")
194 (defvar schedule-diary-entries nil
195 "A list of diary entries in a period.")
197 (defvar schedule-diary-entries-begin nil
198 "The time of the beginning of `schedule-diary-entries'.")
200 (defvar schedule-holiday-list nil
201 "A list of dates on which holidays will fall.")
203 (defvar schedule-initialized nil
204 "Non-nil if the scheduling code has been initialized.")
209 (defun schedule-completion-time (then count
)
210 "Advance THEN by COUNT seconds, skipping the weekends and holidays.
211 THEN must not already be in a holiday or non-worktime. Make sure that
212 `schedule-align-now' is called at least once before this function ever
214 (unless schedule-initialized
215 (schedule-initialize))
216 ;; determine how much time is left in the current day
217 (if (and (featurep 'timeclock
)
218 (timeclock-currently-in-p)
219 (> (timeclock-workday-remaining) 0))
220 (setq schedule-day-remainder
(timeclock-workday-remaining))
221 (setq then
(schedule-align-now then
)
222 schedule-day-remainder
223 (cdr (schedule-nearest-workday then
))))
224 ;; now calculate when the timeframe will actually end
226 (if (< count schedule-day-remainder
)
227 (setq then
(schedule-time-add-seconds then count
)
228 schedule-day-remainder
229 (- schedule-day-remainder
232 (setq count
(- count schedule-day-remainder
)
233 then
(schedule-align-now (schedule-advance-day then
))
234 schedule-day-remainder
235 (cdr (schedule-nearest-workday then
)))))
238 ;;; Internal Functions:
240 (defun schedule-initialize ()
241 "Initialize the scheduling computation code."
242 ;; initialize the cached diary entry lists
243 (setq schedule-diary-entries nil
244 schedule-diary-entries-begin nil
)
246 ;; if someone changes these values outside the customize buffer,
247 ;; they will have to reload this module.
248 (unless schedule-computed-week
249 (schedule-calculate-week))
251 ;; determine the holidays that will apply
252 (setq schedule-holiday-list nil
)
253 (let ((h calendar-holidays
))
255 (if (eq (caar h
) 'holiday-fixed
)
256 (setq schedule-holiday-list
257 (cons (list (nth 1 (car h
))
259 schedule-holiday-list
)))
262 (setq schedule-initialized t
))
264 (defsubst schedule-time-add-seconds
(time seconds
)
265 "To TIME, add SECONDS. Return result as a time value."
266 (let* ((secint (truncate seconds
))
267 (hi (/ secint
65536))
268 (lo (% secint
65536))
269 (calc (+ (cadr time
) lo
)))
271 (list (+ (car time
) hi
) calc
)
272 (list (+ (car time
) (1+ hi
)) (% calc
65536)))))
274 (defsubst schedule-advance-day
(then &optional count
)
275 "Given a time THEN, advance it by COUNT days."
276 (schedule-time-add-seconds then
(* 86400 (or count
1))))
278 (defsubst schedule-time-to-seconds
(time)
279 "Convert TIME to a floating point number."
280 (+ (* (car time
) 65536.0)
282 (/ (or (car (cdr (cdr time
))) 0) 1000000.0)))
284 (defsubst schedule-seconds-to-time
(seconds)
285 "Convert SECONDS (a floating point number) to an Emacs time structure."
286 (list (floor seconds
65536)
287 (floor (mod seconds
65536))
288 (floor (* (- seconds
(ffloor seconds
)) 1000000))))
290 (defsubst schedule-time-diff
(t1 t2
)
291 "Return the difference in seconds between T1 and T2."
292 (- (schedule-time-to-seconds t1
)
293 (schedule-time-to-seconds t2
)))
295 (defsubst schedule-time-less-p
(t1 t2
)
296 "Say whether time T1 is less than time T2."
297 (or (< (car t1
) (car t2
))
298 (and (= (car t1
) (car t2
))
299 (< (nth 1 t1
) (nth 1 t2
)))))
301 (defsubst schedule-time-date
(then)
302 "Return the DATE part of THEN, in calendar friendly format."
303 (let* ((elems (decode-time then
)))
308 (defun schedule-seconds-to-duration (seconds)
309 "Convert SECONDS to a compact time string."
311 (cond ((< seconds
60)
312 (format "%ds" seconds
))
314 (format "%.1fm" (/ (float seconds
) 60.0)))
315 ((< seconds schedule-workday
)
316 (format "%.1fh" (/ (float seconds
) 3600.0)))
317 ((< seconds schedule-week-length
)
318 (format "%.1fd" (/ (float seconds
) schedule-workday
)))
321 (/ (float seconds
) schedule-week-length
))))))
322 (if (string-match "\\.0\\([mhdw]\\)" string
)
323 (replace-match "\\1" t nil string
)
326 (defun schedule-day-begin (then)
327 "Given a time THEN, return its beginning time and length.
328 `schedule-week' is consulted to determine what the typical begin time
329 and length are for a given day of the week. The return value is a
330 cons cell, with the car being a time value, and the cdr the number of
331 seconds during that day."
332 (let* ((elems (decode-time then
))
334 (today (nth dow schedule-computed-week
)))
337 (cons (apply 'encode-time
0 (cadr today
) (car today
)
341 ;; This is from "cal-desk-calendar.el". It should be part of Emacs.
342 (defun schedule-diary-entry-times (s)
343 "List of times at beginning of string S in military-style integers.
344 For example, returns 1325 for 1:25pm. Returns -9999 if no time is
345 recognized. The recognized forms are XXXX or X:XX or XX:XX (military
346 time), XXam or XXpm, and XX:XXam or XX:XXpm. If a range is given, the
347 list contains two elements which will be the start and end of the
348 range. If only one time is given, both elements of the list will
349 contain the time given."
351 ;; Hour and minute range XX:XX-XX:XX[ap]m
353 "^[ ]*\\([0-9]?[0-9]\\):\\([0-9][0-9]\\)-\\([0-9]?[0-9]\\):\\([0-9][0-9]\\)\\([ap]\\)m\\>"
356 (+ (* 100 (%
(string-to-int
357 (substring s
(match-beginning 1) (match-end 1)))
359 (string-to-int (substring s
(match-beginning 2) (match-end 2)))
360 (if (string-equal "a"
361 (substring s
(match-beginning 5) (match-end 5)))
363 (+ (* 100 (%
(string-to-int
364 (substring s
(match-beginning 3) (match-end 3)))
366 (string-to-int (substring s
(match-beginning 4) (match-end 4)))
367 (if (string-equal "a"
368 (substring s
(match-beginning 5) (match-end 5)))
370 (substring s
(+ 2 (match-end 5)))))
372 ;; Military time range
374 "^[ ]*\\([0-9]?[0-9]\\):?\\([0-9][0-9]\\)-\\([0-9]?[0-9]\\):?\\([0-9][0-9]\\)\\(\\|[^ap]\\)"
377 (+ (* 100 (string-to-int
378 (substring s
(match-beginning 1) (match-end 1))))
379 (string-to-int (substring s
(match-beginning 2) (match-end 2))))
380 (+ (* 100 (string-to-int
381 (substring s
(match-beginning 3) (match-end 3))))
382 (string-to-int (substring s
(match-beginning 4) (match-end 4))))
383 (substring s
(1+ (match-end 4)))))
385 ;; Hour range HH[ap]m-HH[ap]m
387 "^[ ]*\\([0-9]?[0-9]\\)\\([ap]\\)m-\\([0-9]?[0-9]\\)\\([ap]\\)m\\>" s
)
389 (+ (* 100 (%
(string-to-int
390 (substring s
(match-beginning 1) (match-end 1)))
392 (if (string-equal "a"
393 (substring s
(match-beginning 2) (match-end 2)))
395 (+ (* 100 (%
(string-to-int
396 (substring s
(match-beginning 3) (match-end 3)))
398 (if (string-equal "a"
399 (substring s
(match-beginning 4) (match-end 4)))
401 (substring s
(+ 2 (match-end 4)))))
403 ;; Hour range HH-HH[ap]m
405 "^[ ]*\\([0-9]?[0-9]\\)-\\([0-9]?[0-9]\\)\\([ap]\\)m\\>" s
)
407 (+ (* 100 (%
(string-to-int
408 (substring s
(match-beginning 1) (match-end 1)))
410 (if (string-equal "a"
411 (substring s
(match-beginning 3) (match-end 3)))
413 (+ (* 100 (%
(string-to-int
414 (substring s
(match-beginning 2) (match-end 2)))
416 (if (string-equal "a"
417 (substring s
(match-beginning 3) (match-end 3)))
419 (substring s
(+ 2 (match-end 3)))))
421 ;; Hour and minute range XX:XX[ap]m-XX:XX[ap]m
423 "^[ ]*\\([0-9]?[0-9]\\):\\([0-9][0-9]\\)\\([ap]\\)m-\\([0-9]?[0-9]\\):\\([0-9][0-9]\\)\\([ap]\\)m\\>"
426 (+ (* 100 (%
(string-to-int
427 (substring s
(match-beginning 1) (match-end 1)))
429 (string-to-int (substring s
(match-beginning 2) (match-end 2)))
430 (if (string-equal "a"
431 (substring s
(match-beginning 3) (match-end 3)))
433 (+ (* 100 (%
(string-to-int
434 (substring s
(match-beginning 4) (match-end 4)))
436 (string-to-int (substring s
(match-beginning 5) (match-end 5)))
437 (if (string-equal "a"
438 (substring s
(match-beginning 6) (match-end 6)))
440 (substring s
(+ 2 (match-end 6)))))
444 "^[ ]*\\([0-9]?[0-9]\\):?\\([0-9][0-9]\\)\\(\\>\\|[^ap]\\)" s
)
445 (let ((time (+ (* 100 (string-to-int
446 (substring s
(match-beginning 1)
448 (string-to-int (substring s
(match-beginning 2)
450 (list time time
(substring s
(1+ (match-end 2))))))
452 ;; Hour only XXam or XXpm
454 "^[ ]*\\([0-9]?[0-9]\\)\\([ap]\\)m\\>" s
)
455 (let ((time (+ (* 100 (%
(string-to-int
456 (substring s
(match-beginning 1) (match-end 1)))
459 "a" (substring s
(match-beginning 2) (match-end 2)))
461 (list time time
(substring s
(+ 2 (match-end 2))))))
463 ;; Hour and minute XX:XXam or XX:XXpm
465 "^[ ]*\\([0-9]?[0-9]\\):\\([0-9][0-9]\\)\\([ap]\\)m\\>" s
)
466 (let ((time (+ (* 100 (%
(string-to-int
467 (substring s
(match-beginning 1)
470 (string-to-int (substring s
(match-beginning 2)
473 "a" (substring s
(match-beginning 3) (match-end 3)))
475 (list time time
(substring s
(+ 2 (match-end 3))))))
477 ;; Sunrise/sunset produced by %%(diary-sunrise-sunset)
479 "^[ ]*Sunrise \\([0-9]?[0-9]\\):\\([0-9][0-9]\\)\\([ap]\\)m\\> ([A-Za-z0-9+-]*), sunset \\([0-9]?[0-9]\\):\\([0-9][0-9]\\)\\([ap]\\)m\\> ([A-Za-z0-9+-]*)\\(.*\\)" s
)
481 (+ (* 100 (%
(string-to-int
482 (substring s
(match-beginning 1) (match-end 1)))
484 (string-to-int (substring s
(match-beginning 2) (match-end 2)))
485 (if (string-equal "a"
486 (substring s
(match-beginning 3) (match-end 3)))
489 (+ (* 100 (%
(string-to-int
490 (substring s
(match-beginning 4) (match-end 4)))
492 (string-to-int (substring s
(match-beginning 5) (match-end 5)))
493 (if (string-equal "a"
494 (substring s
(match-beginning 6) (match-end 6)))
496 (list sunrise-time sunrise-time
498 (substring s
(match-beginning 1) (match-end 2)) "am"
499 (substring s
(1+ (match-end 6)))
500 (substring s
(match-beginning 7) (match-end 7)))
501 sunset-time sunset-time
503 (substring s
(match-beginning 4) (match-end 5)) "pm"
504 (substring s
(1+ (match-end 6)))
505 (substring s
(match-beginning 7) (match-end 7))))))
507 ;; Lunar phase produced by %%(diary-phases-of-moon)
509 "^[ ]*\\(New\\|First Quarter\\|Full\\|Last Quarter\\) Moon \\([0-9]?[0-9]\\):\\([0-9][0-9]\\)\\([ap]\\)m\\> ([A-Z0-9+-]*)" s
)
511 (+ (* 100 (%
(string-to-int
512 (substring s
(match-beginning 2) (match-end 2)))
514 (string-to-int (substring s
(match-beginning 3) (match-end 3)))
516 "a" (substring s
(match-beginning 4) (match-end 4)))
520 ;; Equinox/Solstice produced by %%(diary-equinoxes-solstices)
522 "^[ ]*\\(Vernal Equinox\\|Summer Solstice\\|Autumnal Equinox\\|Winter Solstice\\) \\([0-9]?[0-9]\\):\\([0-9][0-9]\\)\\([ap]\\)m\\> ([A-Z0-9+-]*)" s
)
524 (+ (* 100 (%
(string-to-int
525 (substring s
(match-beginning 2) (match-end 2)))
527 (string-to-int (substring s
(match-beginning 3) (match-end 3)))
529 "a" (substring s
(match-beginning 4) (match-end 4)))
534 (t (list -
9999 -
9999 s
))))
536 (defun schedule-convert-diary-time (date diary-time
)
537 "Convert the given DATE and DIARY-TIME into a time value.
538 DIARY-TIME is an integer of the form HHMM, as returned by
539 `schedule-diary-entry-times'."
540 (let ((minutes (* (+ (* (/ diary-time
100) 60)
541 (% diary-time
100)) 60)))
542 (encode-time 0 (% minutes
60) (truncate (/ minutes
60))
543 (cadr date
) (car date
) (nth 2 date
) nil
)))
545 (defun schedule-get-diary-entries (then)
546 "Find if there are any diary entries occurring THEN (a time value).
547 Return the amount of time they are scheduled to consume."
548 (let ((then-date (schedule-time-date then
))
549 (day-length (schedule-day-begin then
))
550 (diff (and schedule-diary-entries-begin
552 (/ (schedule-time-diff
553 then schedule-diary-entries-begin
) 86400)))))
554 (if (or (not schedule-diary-entries
)
555 (> diff schedule-diary-period
))
556 (let ((display-hook diary-display-hook
))
558 (save-window-excursion
559 (setq diary-display-hook nil
560 schedule-diary-entries
561 (list-diary-entries then-date
562 schedule-diary-period
)
563 schedule-diary-entries-begin then
))
564 (setq diary-display-hook display-hook
))))
565 (let ((entry schedule-diary-entries
)
568 (let ((date (caar entry
)))
569 (if (equal date then-date
)
570 (let* ((times (schedule-diary-entry-times
572 (first (schedule-convert-diary-time
573 (cadr (car entry
)) (car times
)))
574 (last (schedule-convert-diary-time
575 (cadr (car entry
)) (cadr times
))))
576 (if (and (schedule-time-less-p (car day-length
)
578 (schedule-time-less-p
579 last
(schedule-time-add-seconds
580 (car day-length
) (cdr day-length
))))
582 (+ length
(- (schedule-time-diff last first
))))))))
583 (setq entry
(cdr entry
)))
586 (defun schedule-nearest-workday (then)
587 "Given a time THEN, find the nearest workday."
589 (while (and (> max
0)
590 (setq entry
(schedule-day-begin then
))
591 (or (not entry
) (= (cdr entry
) 0)))
592 (setq then
(schedule-advance-day then
)
595 (error "There are is no work time defined during the week"))
596 (and schedule-diary-period
597 (setcdr entry
(- (cdr entry
)
598 (schedule-get-diary-entries then
))))
601 (defun schedule-nearest-true-workday (then)
602 "Given a time THEN, find the nearest real workday (not a holiday)."
603 (let ((max 365) entry
)
604 (while (and (> max
0)
605 (setq entry
(schedule-nearest-workday then
))
606 ;; jww (1999-04-23): this will need to be updated to
607 ;; handle floating holidays
608 (let* ((date (schedule-time-date (car entry
)))
609 (mon-day (list (car date
) (cadr date
))))
610 (member mon-day schedule-holiday-list
)))
611 (setq then
(car entry
)
612 then
(schedule-advance-day then
)
615 (error "There is no time available for at least a year"))
618 (defun schedule-align-now (then)
619 "Given a time THEN, move it ahead to the next valid moment."
620 (let ((day (schedule-nearest-true-workday then
)))
621 (if (schedule-time-less-p then
(car day
))
623 (if (> (- (schedule-time-diff then
(car day
)))
625 (car (schedule-nearest-true-workday
626 (schedule-advance-day then
)))
631 ;;; schedule.el ends here